Skip to content

Commit a65dac9

Browse files
committed
Add AI Prompt generator
1 parent 025d939 commit a65dac9

File tree

5 files changed

+203
-34
lines changed

5 files changed

+203
-34
lines changed

AiServer.ServiceModel/OpenAiChatServer.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public class GetModelImage : IGet, IReturn<byte[]>
7070

7171
[Tag(ServiceModel.Tag.OpenAi)]
7272
[ValidateApiKey]
73+
[SystemJson(UseSystemJson.Response)]
7374
public class QueueOpenAiChatCompletion : IReturn<QueueOpenAiChatResponse>
7475
{
7576
public string? RefId { get; set; }
@@ -91,6 +92,7 @@ public class QueueOpenAiChatResponse
9192
[Tag(ServiceModel.Tag.OpenAi)]
9293
[ValidateApiKey]
9394
[Route("/v1/chat/completions", "POST")]
95+
[SystemJson(UseSystemJson.Response)]
9496
public class OpenAiChatCompletion : OpenAiChat, IPost, IReturn<OpenAiChatResponse>
9597
{
9698
public string? RefId { get; set; }

AiServer/wwwroot/css/app.css

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,10 @@ select{
10691069
margin-left: -0.25rem;
10701070
}
10711071

1072+
.-ml-3 {
1073+
margin-left: -0.75rem;
1074+
}
1075+
10721076
.-ml-px {
10731077
margin-left: -1px;
10741078
}
@@ -1089,6 +1093,10 @@ select{
10891093
margin-top: -2rem;
10901094
}
10911095

1096+
.mb-0 {
1097+
margin-bottom: 0px;
1098+
}
1099+
10921100
.mb-1 {
10931101
margin-bottom: 0.25rem;
10941102
}
@@ -1536,6 +1544,11 @@ select{
15361544
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
15371545
}
15381546

1547+
.rotate-90 {
1548+
--tw-rotate: 90deg;
1549+
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1550+
}
1551+
15391552
.rotate-\[30deg\] {
15401553
--tw-rotate: 30deg;
15411554
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -2191,6 +2204,10 @@ select{
21912204
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
21922205
}
21932206

2207+
.bg-white\/0 {
2208+
background-color: rgb(255 255 255 / 0);
2209+
}
2210+
21942211
.bg-yellow-50 {
21952212
--tw-bg-opacity: 1;
21962213
background-color: rgb(254 252 232 / var(--tw-bg-opacity));
@@ -2853,6 +2870,11 @@ select{
28532870
color: rgb(100 116 139 / var(--tw-text-opacity));
28542871
}
28552872

2873+
.text-slate-900 {
2874+
--tw-text-opacity: 1;
2875+
color: rgb(15 23 42 / var(--tw-text-opacity));
2876+
}
2877+
28562878
.text-teal-700 {
28572879
--tw-text-opacity: 1;
28582880
color: rgb(15 118 110 / var(--tw-text-opacity));
@@ -3009,6 +3031,10 @@ select{
30093031
--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity));
30103032
}
30113033

3034+
.ring-slate-900\/10 {
3035+
--tw-ring-color: rgb(15 23 42 / 0.1);
3036+
}
3037+
30123038
.ring-white {
30133039
--tw-ring-opacity: 1;
30143040
--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity));
@@ -3103,6 +3129,10 @@ select{
31033129
transition-duration: 100ms;
31043130
}
31053131

3132+
.duration-150 {
3133+
transition-duration: 150ms;
3134+
}
3135+
31063136
.duration-200 {
31073137
transition-duration: 200ms;
31083138
}
@@ -3308,6 +3338,10 @@ select{
33083338
background-color: rgb(3 105 161 / var(--tw-bg-opacity));
33093339
}
33103340

3341+
.hover\:bg-white\/25:hover {
3342+
background-color: rgb(255 255 255 / 0.25);
3343+
}
3344+
33113345
.hover\:bg-yellow-50:hover {
33123346
--tw-bg-opacity: 1;
33133347
background-color: rgb(254 252 232 / var(--tw-bg-opacity));
@@ -3323,9 +3357,9 @@ select{
33233357
color: rgb(21 94 117 / var(--tw-text-opacity));
33243358
}
33253359

3326-
.hover\:text-gray-200:hover {
3360+
.hover\:text-gray-100:hover {
33273361
--tw-text-opacity: 1;
3328-
color: rgb(229 231 235 / var(--tw-text-opacity));
3362+
color: rgb(243 244 246 / var(--tw-text-opacity));
33293363
}
33303364

33313365
.hover\:text-gray-400:hover {
@@ -3398,11 +3432,6 @@ select{
33983432
color: rgb(7 89 133 / var(--tw-text-opacity));
33993433
}
34003434

3401-
.hover\:text-gray-100:hover {
3402-
--tw-text-opacity: 1;
3403-
color: rgb(243 244 246 / var(--tw-text-opacity));
3404-
}
3405-
34063435
.hover\:underline:hover {
34073436
text-decoration-line: underline;
34083437
}
@@ -3411,6 +3440,10 @@ select{
34113440
opacity: 0.7;
34123441
}
34133442

3443+
.hover\:ring-slate-900\/15:hover {
3444+
--tw-ring-color: rgb(15 23 42 / 0.15);
3445+
}
3446+
34143447
.hover\:drop-shadow:hover {
34153448
--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));
34163449
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);

AiServer/wwwroot/mjs/components/OpenAiChat.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ export default {
264264
const api = await client.api(request)
265265

266266
error.value = api.error
267-
console.log(api.response, api.error)
267+
console.debug('chat.sendMessage', api.response, api.error)
268268
const chatResponse = api.response
269269
if (chatResponse?.id) {
270270
chatResponse.request = request
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { ref, computed, onMounted } from "vue"
2+
import { useClient } from "@servicestack/vue"
3+
import { createErrorStatus } from "@servicestack/client"
4+
import { ActiveAiModels, QueryPrompts, OpenAiChatCompletion } from "dtos"
5+
6+
export default {
7+
template:`
8+
<div v-if="system">
9+
<button @click="show=!show" type="button" class="-ml-3 bg-white text-gray-600 hover:text-gray-900 group w-full flex items-center pr-2 py-2 text-left text-sm font-medium">
10+
<svg v-if="show" class="text-gray-400 rotate-90 mr-0.5 flex-shrink-0 h-5 w-5 transform group-hover:text-gray-400 transition-colors ease-in-out duration-150" viewBox="0 0 20 20" aria-hidden="true"><path d="M6 6L14 10L6 14V6Z" fill="currentColor"></path></svg>
11+
<svg v-else class="text-gray-300 mr-0.5 flex-shrink-0 h-5 w-5 transform group-hover:text-gray-400 transition-colors ease-in-out duration-150" viewBox="0 0 20 20" aria-hidden="true"><path d="M6 6L14 10L6 14V6Z" fill="currentColor"></path></svg>
12+
AI Prompt Generator
13+
</button>
14+
<div v-if="show">
15+
<form class="grid grid-cols-6 gap-4" @submit.prevent="send()" :disabled="!validPrompt">
16+
<div class="col-span-6 sm:col-span-2">
17+
<TextInput id="subject" v-model="subject" label="subject" placeholder="Use AI to generate image prompts for..." />
18+
</div>
19+
<div class="col-span-6 sm:col-span-2">
20+
<Autocomplete id="model" :options="models" v-model="model" label="model"
21+
:match="(x, value) => x.toLowerCase().includes(value.toLowerCase())"
22+
placeholder="Select Model...">
23+
<template #item="name">
24+
<div class="flex items-center">
25+
<Icon class="h-6 w-6 flex-shrink-0" :src="'/icons/models/' + name" loading="lazy" />
26+
<span class="ml-3 truncate">{{name}}</span>
27+
</div>
28+
</template>
29+
</Autocomplete>
30+
</div>
31+
<div class="col-span-6 sm:col-span-1">
32+
<TextInput type="number" id="count" v-model="count" label="count" min="1" />
33+
</div>
34+
<div class="col-span-6 sm:col-span-1 align-bottom">
35+
<div>&nbsp;</div>
36+
<PrimaryButton :disabled="!validPrompt">Generate</PrimaryButton>
37+
</div>
38+
</form>
39+
<Loading v-if="client.loading.value">Asking {{model}}...</Loading>
40+
<ErrorSummary v-else-if="error" :status="error" />
41+
<div v-else-if="results.length" class="mt-4">
42+
<div v-for="result in results" @click="$emit('selected',result)" class="message mb-2 cursor-pointer rounded-lg inline-flex justify-center rounded-lg text-sm py-3 px-4 bg-gray-50 text-slate-900 ring-1 ring-slate-900/10 hover:bg-white/25 hover:ring-slate-900/15">
43+
{{result}}
44+
</div>
45+
</div>
46+
</div>
47+
</div>
48+
`,
49+
emits:['selected'],
50+
props: {
51+
promptId: String,
52+
systemPrompt: String,
53+
},
54+
setup(props) {
55+
const client = useClient()
56+
const request = ref(new OpenAiChatCompletion({ }))
57+
const system = ref(props.systemPrompt)
58+
const subject = ref('')
59+
const defaults = {
60+
show: false,
61+
model: 'gemini-flash',
62+
count: 3,
63+
}
64+
const prefsKey = 'img2txt.gen.prefs'
65+
const prefs = JSON.parse(localStorage.getItem(prefsKey) ?? JSON.stringify(defaults))
66+
const show = ref(prefs.show)
67+
const count = ref(prefs.count)
68+
const model = ref(prefs.model)
69+
const error = ref()
70+
const models = ref([])
71+
const results = ref([])
72+
const validPrompt = computed(() => subject.value && model.value && count.value)
73+
74+
function savePrefs() {
75+
prefs.show = show.value
76+
prefs.model = model.value
77+
prefs.count = count.value
78+
localStorage.setItem(prefsKey, JSON.stringify(prefs))
79+
}
80+
81+
if (!system.value && props.promptId) {
82+
onMounted(async () => {
83+
const apiPrompt = await client.api(new QueryPrompts({
84+
id: props.promptId
85+
}))
86+
system.value = apiPrompt?.response?.results?.[0]
87+
88+
const api = await client.api(new ActiveAiModels())
89+
models.value = await api.response.results
90+
models.value.sort((a,b) => a.localeCompare(b))
91+
})
92+
}
93+
94+
async function send() {
95+
if (!validPrompt.value) return
96+
savePrefs()
97+
98+
const content = `Provide ${count.value} great descriptive prompts to generate images of ${subject.value} in Stable Diffusion SDXL and Mid Journey. Respond with only the prompts in a JSON array. Example ["prompt1","prompt2"]`
99+
100+
const msgs = [
101+
{ role:'system', content:system.value },
102+
{ role:'user', content },
103+
]
104+
105+
const request = new OpenAiChatCompletion({
106+
tag: "admin",
107+
model: model.value,
108+
messages: msgs,
109+
temperature: 0.7,
110+
maxTokens: 2048,
111+
})
112+
error.value = null
113+
const api = await client.api(request)
114+
error.value = api.error
115+
if (api.response) {
116+
let json = api.response?.choices[0]?.message?.content?.trim() ?? ''
117+
console.debug(api.response)
118+
if (json) {
119+
results.value = []
120+
const docPrefix = '```json'
121+
if (json.startsWith(docPrefix)) {
122+
json = json.substring(docPrefix.length, json.length - 3)
123+
}
124+
try {
125+
console.log('json', json)
126+
const obj = JSON.parse(json)
127+
if (Array.isArray(obj)) {
128+
results.value = obj
129+
}
130+
} catch(e) {
131+
console.warn('could not parse json', e, json)
132+
}
133+
}
134+
if (!results.value.length) {
135+
error.value = createErrorStatus('Could not parse prompts')
136+
}
137+
}
138+
}
139+
140+
return { client, system, request, show, subject, count, models, model, error, results, validPrompt, send }
141+
}
142+
}

0 commit comments

Comments
 (0)