llms-py 2.0.9__tar.gz → 2.0.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {llms_py-2.0.9/llms_py.egg-info → llms_py-2.0.10}/PKG-INFO +1 -1
- llms_py-2.0.10/index.html +80 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/llms.json +1 -4
- {llms_py-2.0.9 → llms_py-2.0.10}/llms.py +14 -7
- {llms_py-2.0.9 → llms_py-2.0.10/llms_py.egg-info}/PKG-INFO +1 -1
- {llms_py-2.0.9 → llms_py-2.0.10}/llms_py.egg-info/SOURCES.txt +12 -2
- {llms_py-2.0.9 → llms_py-2.0.10}/pyproject.toml +1 -1
- {llms_py-2.0.9 → llms_py-2.0.10}/setup.py +16 -6
- llms_py-2.0.10/ui/Avatar.mjs +28 -0
- llms_py-2.0.10/ui/Brand.mjs +23 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/ChatPrompt.mjs +101 -69
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/Main.mjs +43 -183
- llms_py-2.0.10/ui/ModelSelector.mjs +29 -0
- llms_py-2.0.10/ui/ProviderStatus.mjs +105 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/Recents.mjs +2 -1
- llms_py-2.0.10/ui/SettingsDialog.mjs +374 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/Sidebar.mjs +11 -27
- llms_py-2.0.10/ui/SignIn.mjs +64 -0
- llms_py-2.0.10/ui/SystemPromptEditor.mjs +31 -0
- llms_py-2.0.10/ui/SystemPromptSelector.mjs +36 -0
- llms_py-2.0.10/ui/Welcome.mjs +8 -0
- llms_py-2.0.10/ui/ai.mjs +80 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/app.css +76 -10
- llms_py-2.0.10/ui/lib/servicestack-vue.mjs +37 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/markdown.mjs +9 -2
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/tailwind.input.css +13 -4
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/threadStore.mjs +2 -2
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/typography.css +109 -1
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/utils.mjs +8 -2
- llms_py-2.0.9/index.html +0 -64
- llms_py-2.0.9/ui/lib/servicestack-vue.min.mjs +0 -37
- {llms_py-2.0.9 → llms_py-2.0.10}/LICENSE +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/MANIFEST.in +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/README.md +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/llms_py.egg-info/dependency_links.txt +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/llms_py.egg-info/entry_points.txt +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/llms_py.egg-info/not-zip-safe +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/llms_py.egg-info/requires.txt +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/llms_py.egg-info/top_level.txt +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/requirements.txt +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/setup.cfg +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/App.mjs +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/fav.svg +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/lib/highlight.min.mjs +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/lib/idb.min.mjs +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/lib/marked.min.mjs +0 -0
- /llms_py-2.0.9/ui/lib/servicestack-client.min.mjs → /llms_py-2.0.10/ui/lib/servicestack-client.mjs +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/lib/vue-router.min.mjs +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/lib/vue.min.mjs +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui/lib/vue.mjs +0 -0
- {llms_py-2.0.9 → llms_py-2.0.10}/ui.json +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<html>
|
|
2
|
+
<head>
|
|
3
|
+
<title>llms.py</title>
|
|
4
|
+
<link rel="stylesheet" href="/ui/typography.css">
|
|
5
|
+
<link rel="stylesheet" href="/ui/app.css">
|
|
6
|
+
<link rel="icon" type="image/svg" href="/ui/fav.svg">
|
|
7
|
+
<style>
|
|
8
|
+
[type='button'],button[type='submit']{cursor:pointer}
|
|
9
|
+
[type='checkbox'].switch:checked:hover,
|
|
10
|
+
[type='checkbox'].switch:checked:focus,
|
|
11
|
+
[type='checkbox'].switch:checked,
|
|
12
|
+
[type='checkbox'].switch:focus,
|
|
13
|
+
[type='checkbox'].switch
|
|
14
|
+
{
|
|
15
|
+
border: none;
|
|
16
|
+
background: none;
|
|
17
|
+
outline: none;
|
|
18
|
+
box-shadow: none;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<script type="importmap">
|
|
24
|
+
{
|
|
25
|
+
"imports": {
|
|
26
|
+
"vue": "/ui/lib/vue.min.mjs",
|
|
27
|
+
"vue-router": "/ui/lib/vue-router.min.mjs",
|
|
28
|
+
"@servicestack/client": "/ui/lib/servicestack-client.mjs",
|
|
29
|
+
"@servicestack/vue": "/ui/lib/servicestack-vue.mjs",
|
|
30
|
+
"idb": "/ui/lib/idb.min.mjs",
|
|
31
|
+
"marked": "/ui/lib/marked.min.mjs",
|
|
32
|
+
"highlight.js": "/ui/lib/highlight.min.mjs"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
<body>
|
|
37
|
+
<div id="app"></div>
|
|
38
|
+
</body>
|
|
39
|
+
<script type="module">
|
|
40
|
+
import { createApp, defineAsyncComponent } from 'vue'
|
|
41
|
+
import { createWebHistory, createRouter } from "vue-router"
|
|
42
|
+
import ServiceStackVue from "@servicestack/vue"
|
|
43
|
+
import App from '/ui/App.mjs'
|
|
44
|
+
import ai from '/ui/ai.mjs'
|
|
45
|
+
import SettingsDialog from '/ui/SettingsDialog.mjs'
|
|
46
|
+
|
|
47
|
+
const { config, models } = await ai.init()
|
|
48
|
+
const MainComponent = defineAsyncComponent(() => import(ai.base + '/ui/Main.mjs'))
|
|
49
|
+
const RecentsComponent = defineAsyncComponent(() => import(ai.base + '/ui/Recents.mjs'))
|
|
50
|
+
|
|
51
|
+
const Components = {
|
|
52
|
+
SettingsDialog,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const routes = [
|
|
56
|
+
{ path: '/', component: MainComponent },
|
|
57
|
+
{ path: '/c/:id', component: MainComponent },
|
|
58
|
+
{ path: '/recents', component: RecentsComponent },
|
|
59
|
+
{ path: '/:fallback(.*)*', component: MainComponent }
|
|
60
|
+
]
|
|
61
|
+
routes.forEach(r => r.path = ai.base + r.path)
|
|
62
|
+
const router = createRouter({
|
|
63
|
+
history: createWebHistory(),
|
|
64
|
+
routes,
|
|
65
|
+
})
|
|
66
|
+
const app = createApp(App, { config, models })
|
|
67
|
+
app.use(router)
|
|
68
|
+
app.use(ServiceStackVue)
|
|
69
|
+
app.provide('ai', ai)
|
|
70
|
+
app.provide('config', config)
|
|
71
|
+
app.provide('models', models)
|
|
72
|
+
Object.keys(Components).forEach(name => {
|
|
73
|
+
app.component(name, Components[name])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
window.ai = app.config.globalProperties.$ai = ai
|
|
77
|
+
|
|
78
|
+
app.mount('#app')
|
|
79
|
+
</script>
|
|
80
|
+
</html>
|
|
@@ -95,10 +95,8 @@
|
|
|
95
95
|
"deepseek-r1:671b": "deepseek/deepseek-r1-0528:free",
|
|
96
96
|
"gemini-2.0-flash": "google/gemini-2.0-flash-exp:free",
|
|
97
97
|
"glm-4.5-air": "z-ai/glm-4.5-air:free",
|
|
98
|
-
"grok-4-fast": "x-ai/grok-4-fast:free",
|
|
99
98
|
"mai-ds-r1": "microsoft/mai-ds-r1:free",
|
|
100
99
|
"llama3.3:70b": "meta-llama/llama-3.3-70b-instruct:free",
|
|
101
|
-
"kimi-k2": "moonshotai/kimi-k2:free",
|
|
102
100
|
"nemotron-nano:9b": "nvidia/nemotron-nano-9b-v2:free",
|
|
103
101
|
"deepseek-r1-distill-llama:70b": "deepseek/deepseek-r1-distill-llama-70b:free",
|
|
104
102
|
"gpt-oss:20b": "openai/gpt-oss-20b:free",
|
|
@@ -107,7 +105,6 @@
|
|
|
107
105
|
"devstral-small": "mistralai/devstral-small-2505:free",
|
|
108
106
|
"venice-uncensored:24b": "cognitivecomputations/dolphin-mistral-24b-venice-edition:free",
|
|
109
107
|
"llama3.3:8b": "meta-llama/llama-3.3-8b-instruct:free",
|
|
110
|
-
"llama3.1:405b": "meta-llama/llama-3.1-405b-instruct:free",
|
|
111
108
|
"kimi-dev:72b": "moonshotai/kimi-dev-72b:free",
|
|
112
109
|
"gemma3:27b": "google/gemma-3-27b-it:free",
|
|
113
110
|
"qwen3-coder": "qwen/qwen3-coder:free",
|
|
@@ -176,7 +173,7 @@
|
|
|
176
173
|
}
|
|
177
174
|
},
|
|
178
175
|
"ollama": {
|
|
179
|
-
"enabled":
|
|
176
|
+
"enabled": true,
|
|
180
177
|
"type": "OllamaProvider",
|
|
181
178
|
"base_url": "http://localhost:11434",
|
|
182
179
|
"models": {},
|
|
@@ -22,7 +22,7 @@ from aiohttp import web
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
|
|
24
24
|
|
|
25
|
-
VERSION = "2.0.
|
|
25
|
+
VERSION = "2.0.10"
|
|
26
26
|
_ROOT = None
|
|
27
27
|
g_config_path = None
|
|
28
28
|
g_ui_path = None
|
|
@@ -64,8 +64,8 @@ def chat_summary(chat):
|
|
|
64
64
|
elif 'file' in item:
|
|
65
65
|
if 'file_data' in item['file']:
|
|
66
66
|
data = item['file']['file_data']
|
|
67
|
-
prefix =
|
|
68
|
-
item['file']['file_data'] = prefix + f",({len(
|
|
67
|
+
prefix = data.split(',', 1)[0]
|
|
68
|
+
item['file']['file_data'] = prefix + f",({len(data) - len(prefix)})"
|
|
69
69
|
return json.dumps(clone, indent=2)
|
|
70
70
|
|
|
71
71
|
def gemini_chat_summary(gemini_chat):
|
|
@@ -444,7 +444,14 @@ class GoogleProvider(OpenAiProvider):
|
|
|
444
444
|
async with aiohttp.ClientSession() as session:
|
|
445
445
|
for message in chat['messages']:
|
|
446
446
|
if message['role'] == 'system':
|
|
447
|
-
|
|
447
|
+
content = message['content']
|
|
448
|
+
if isinstance(content, list):
|
|
449
|
+
for item in content:
|
|
450
|
+
if 'text' in item:
|
|
451
|
+
system_prompt = item['text']
|
|
452
|
+
break
|
|
453
|
+
elif isinstance(content, str):
|
|
454
|
+
system_prompt = content
|
|
448
455
|
elif 'content' in message:
|
|
449
456
|
if isinstance(message['content'], list):
|
|
450
457
|
parts = []
|
|
@@ -520,7 +527,7 @@ class GoogleProvider(OpenAiProvider):
|
|
|
520
527
|
# Add system instruction if present
|
|
521
528
|
if system_prompt is not None:
|
|
522
529
|
gemini_chat['systemInstruction'] = {
|
|
523
|
-
"parts": [{"text": system_prompt
|
|
530
|
+
"parts": [{"text": system_prompt}]
|
|
524
531
|
}
|
|
525
532
|
|
|
526
533
|
if 'stop' in chat:
|
|
@@ -1257,7 +1264,7 @@ def main():
|
|
|
1257
1264
|
app.router.add_route('*', '/{tail:.*}', index_handler)
|
|
1258
1265
|
|
|
1259
1266
|
if os.path.exists(g_ui_path):
|
|
1260
|
-
async def
|
|
1267
|
+
async def ui_config_handler(request):
|
|
1261
1268
|
with open(g_ui_path, "r") as f:
|
|
1262
1269
|
ui = json.load(f)
|
|
1263
1270
|
if 'defaults' not in ui:
|
|
@@ -1269,7 +1276,7 @@ def main():
|
|
|
1269
1276
|
"disabled": disabled
|
|
1270
1277
|
}
|
|
1271
1278
|
return web.json_response(ui)
|
|
1272
|
-
app.router.add_get('/
|
|
1279
|
+
app.router.add_get('/config', ui_config_handler)
|
|
1273
1280
|
|
|
1274
1281
|
print(f"Starting server on port {port}...")
|
|
1275
1282
|
web.run_app(app, host='0.0.0.0', port=port)
|
|
@@ -16,10 +16,20 @@ llms_py.egg-info/not-zip-safe
|
|
|
16
16
|
llms_py.egg-info/requires.txt
|
|
17
17
|
llms_py.egg-info/top_level.txt
|
|
18
18
|
ui/App.mjs
|
|
19
|
+
ui/Avatar.mjs
|
|
20
|
+
ui/Brand.mjs
|
|
19
21
|
ui/ChatPrompt.mjs
|
|
20
22
|
ui/Main.mjs
|
|
23
|
+
ui/ModelSelector.mjs
|
|
24
|
+
ui/ProviderStatus.mjs
|
|
21
25
|
ui/Recents.mjs
|
|
26
|
+
ui/SettingsDialog.mjs
|
|
22
27
|
ui/Sidebar.mjs
|
|
28
|
+
ui/SignIn.mjs
|
|
29
|
+
ui/SystemPromptEditor.mjs
|
|
30
|
+
ui/SystemPromptSelector.mjs
|
|
31
|
+
ui/Welcome.mjs
|
|
32
|
+
ui/ai.mjs
|
|
23
33
|
ui/app.css
|
|
24
34
|
ui/fav.svg
|
|
25
35
|
ui/markdown.mjs
|
|
@@ -30,8 +40,8 @@ ui/utils.mjs
|
|
|
30
40
|
ui/lib/highlight.min.mjs
|
|
31
41
|
ui/lib/idb.min.mjs
|
|
32
42
|
ui/lib/marked.min.mjs
|
|
33
|
-
ui/lib/servicestack-client.
|
|
34
|
-
ui/lib/servicestack-vue.
|
|
43
|
+
ui/lib/servicestack-client.mjs
|
|
44
|
+
ui/lib/servicestack-vue.mjs
|
|
35
45
|
ui/lib/vue-router.min.mjs
|
|
36
46
|
ui/lib/vue.min.mjs
|
|
37
47
|
ui/lib/vue.mjs
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "llms-py"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.10"
|
|
8
8
|
description = "A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -16,7 +16,7 @@ with open(os.path.join(this_directory, "requirements.txt"), encoding="utf-8") as
|
|
|
16
16
|
|
|
17
17
|
setup(
|
|
18
18
|
name="llms-py",
|
|
19
|
-
version="2.0.
|
|
19
|
+
version="2.0.10",
|
|
20
20
|
author="ServiceStack",
|
|
21
21
|
author_email="team@servicestack.net",
|
|
22
22
|
description="A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers",
|
|
@@ -63,18 +63,28 @@ setup(
|
|
|
63
63
|
(
|
|
64
64
|
"ui",
|
|
65
65
|
[
|
|
66
|
+
"ui/ai.mjs",
|
|
67
|
+
"ui/app.css",
|
|
66
68
|
"ui/App.mjs",
|
|
69
|
+
"ui/Avatar.mjs",
|
|
70
|
+
"ui/Brand.mjs",
|
|
67
71
|
"ui/ChatPrompt.mjs",
|
|
72
|
+
"ui/fav.svg",
|
|
68
73
|
"ui/Main.mjs",
|
|
74
|
+
"ui/markdown.mjs",
|
|
75
|
+
"ui/ModelSelector.mjs",
|
|
76
|
+
"ui/ProviderStatus.mjs",
|
|
69
77
|
"ui/Recents.mjs",
|
|
78
|
+
"ui/SettingsDialog.mjs",
|
|
70
79
|
"ui/Sidebar.mjs",
|
|
71
|
-
"ui/
|
|
72
|
-
"ui/
|
|
73
|
-
"ui/
|
|
80
|
+
"ui/SignIn.mjs",
|
|
81
|
+
"ui/SystemPromptEditor.mjs",
|
|
82
|
+
"ui/SystemPromptSelector.mjs",
|
|
74
83
|
"ui/tailwind.input.css",
|
|
75
84
|
"ui/threadStore.mjs",
|
|
76
85
|
"ui/typography.css",
|
|
77
86
|
"ui/utils.mjs",
|
|
87
|
+
"ui/Welcome.mjs",
|
|
78
88
|
],
|
|
79
89
|
),
|
|
80
90
|
(
|
|
@@ -83,8 +93,8 @@ setup(
|
|
|
83
93
|
"ui/lib/highlight.min.mjs",
|
|
84
94
|
"ui/lib/idb.min.mjs",
|
|
85
95
|
"ui/lib/marked.min.mjs",
|
|
86
|
-
"ui/lib/servicestack-client.
|
|
87
|
-
"ui/lib/servicestack-vue.
|
|
96
|
+
"ui/lib/servicestack-client.mjs",
|
|
97
|
+
"ui/lib/servicestack-vue.mjs",
|
|
88
98
|
"ui/lib/vue-router.min.mjs",
|
|
89
99
|
"ui/lib/vue.min.mjs",
|
|
90
100
|
],
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { computed, inject } from "vue"
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
template:`
|
|
5
|
+
<div v-if="$ai.auth?.profileUrl" :title="authTitle">
|
|
6
|
+
<img :src="$ai.auth.profileUrl" class="size-8 rounded-full" />
|
|
7
|
+
</div>
|
|
8
|
+
`,
|
|
9
|
+
setup() {
|
|
10
|
+
const ai = inject('ai')
|
|
11
|
+
const authTitle = computed(() => {
|
|
12
|
+
if (!ai.auth) return ''
|
|
13
|
+
const { userId, userName, displayName, bearerToken, roles } = ai.auth
|
|
14
|
+
const name = userName || displayName
|
|
15
|
+
const prefix = roles && roles.includes('Admin') ? 'Admin' : 'Name'
|
|
16
|
+
const sb = [
|
|
17
|
+
name ? `${prefix}: ${name}` : '',
|
|
18
|
+
`API Key: ${bearerToken}`,
|
|
19
|
+
`${userId}`,
|
|
20
|
+
]
|
|
21
|
+
return sb.filter(x => x).join('\n')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
authTitle,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
template:`
|
|
3
|
+
<div class="flex-shrink-0 px-4 py-4 border-b border-gray-200 bg-white min-h-16 select-none">
|
|
4
|
+
<div class="flex items-center justify-between">
|
|
5
|
+
<button type="button"
|
|
6
|
+
@click="$emit('home')"
|
|
7
|
+
class="text-lg font-semibold text-gray-900 hover:text-blue-600 focus:outline-none transition-colors"
|
|
8
|
+
title="Go back to initial state"
|
|
9
|
+
>
|
|
10
|
+
History
|
|
11
|
+
</button>
|
|
12
|
+
<button type="button"
|
|
13
|
+
@click="$emit('new')"
|
|
14
|
+
class="text-gray-900 hover:text-blue-600 focus:outline-none transition-colors"
|
|
15
|
+
title="New Chat"
|
|
16
|
+
>
|
|
17
|
+
<svg class="size-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z"/></g></svg>
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
`,
|
|
22
|
+
emits:['home','new'],
|
|
23
|
+
}
|
|
@@ -11,7 +11,6 @@ export function useChatPrompt() {
|
|
|
11
11
|
const attachedFiles = ref([])
|
|
12
12
|
const isGenerating = ref(false)
|
|
13
13
|
const errorStatus = ref(null)
|
|
14
|
-
const errorMessage = ref(null)
|
|
15
14
|
const hasImage = () => attachedFiles.value.some(f => imageExts.includes(lastRightPart(f.name, '.')))
|
|
16
15
|
const hasAudio = () => attachedFiles.value.some(f => audioExts.includes(lastRightPart(f.name, '.')))
|
|
17
16
|
const hasFile = () => attachedFiles.value.length > 0
|
|
@@ -28,7 +27,6 @@ export function useChatPrompt() {
|
|
|
28
27
|
messageText,
|
|
29
28
|
attachedFiles,
|
|
30
29
|
errorStatus,
|
|
31
|
-
errorMessage,
|
|
32
30
|
isGenerating,
|
|
33
31
|
get generating() {
|
|
34
32
|
return isGenerating.value
|
|
@@ -44,23 +42,32 @@ export function useChatPrompt() {
|
|
|
44
42
|
export default {
|
|
45
43
|
template:`
|
|
46
44
|
<div class="mx-auto max-w-3xl">
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
<SettingsDialog :isOpen="showSettings" @close="showSettings = false" />
|
|
46
|
+
<div class="flex space-x-2">
|
|
47
|
+
<!-- Attach (+) button and Settings button -->
|
|
48
|
+
<div class="mt-1.5 flex flex-col space-y-1 items-center">
|
|
49
|
+
<div>
|
|
50
|
+
<button type="button"
|
|
51
|
+
@click="triggerFilePicker"
|
|
52
|
+
:disabled="isGenerating || !model"
|
|
53
|
+
class="size-8 flex items-center justify-center rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed"
|
|
54
|
+
title="Attach image or audio">
|
|
55
|
+
<svg class="size-5" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256">
|
|
56
|
+
<path d="M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z"></path>
|
|
57
|
+
</svg>
|
|
58
|
+
</button>
|
|
59
|
+
<!-- Hidden file input -->
|
|
60
|
+
<input ref="fileInput" type="file" multiple @change="onFilesSelected"
|
|
61
|
+
class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<button type="button" title="Settings" @click="showSettings = true"
|
|
52
66
|
:disabled="isGenerating || !model"
|
|
53
|
-
class="
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
58
|
-
</svg>
|
|
59
|
-
</button>
|
|
60
|
-
<!-- Hidden file input -->
|
|
61
|
-
<input ref="fileInput" type="file" multiple @change="onFilesSelected"
|
|
62
|
-
class="hidden" accept="image/*,audio/*,.pdf,.doc,.docx,.xml,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
63
|
-
/>
|
|
67
|
+
class="size-8 flex items-center justify-center rounded-md border border-gray-300 text-gray-600 hover:bg-gray-50 disabled:text-gray-400 disabled:cursor-not-allowed">
|
|
68
|
+
<svg class="size-4 text-gray-600 disabled:text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><path d="M40,88H73a32,32,0,0,0,62,0h81a8,8,0,0,0,0-16H135a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16Zm64-24A16,16,0,1,1,88,80,16,16,0,0,1,104,64ZM216,168H199a32,32,0,0,0-62,0H40a8,8,0,0,0,0,16h97a32,32,0,0,0,62,0h17a8,8,0,0,0,0-16Zm-48,24a16,16,0,1,1,16-16A16,16,0,0,1,168,192Z"></path></svg>
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
64
71
|
</div>
|
|
65
72
|
|
|
66
73
|
<div class="flex-1">
|
|
@@ -70,7 +77,7 @@ export default {
|
|
|
70
77
|
@keydown.enter.exact.prevent="sendMessage"
|
|
71
78
|
@keydown.enter.shift.exact="addNewLine"
|
|
72
79
|
placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
|
|
73
|
-
rows="
|
|
80
|
+
rows="3"
|
|
74
81
|
class="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
75
82
|
:disabled="isGenerating || !model"
|
|
76
83
|
></textarea>
|
|
@@ -90,16 +97,16 @@ export default {
|
|
|
90
97
|
</div>
|
|
91
98
|
</div>
|
|
92
99
|
|
|
93
|
-
<div>
|
|
100
|
+
<div class="pt-3">
|
|
94
101
|
<button title="Send (Enter)" type="button"
|
|
95
102
|
@click="sendMessage"
|
|
96
103
|
:disabled="!messageText.trim() || isGenerating || !model"
|
|
97
|
-
class="
|
|
98
|
-
<svg v-if="isGenerating" class="size-
|
|
104
|
+
class="p-2 flex items-center justify-center rounded-full bg-gray-700 text-white transition-colors hover:opacity-70 focus-visible:outline-none focus-visible:outline-black disabled:bg-[#D7D7D7] disabled:text-[#f4f4f4] disabled:hover:opacity-100 dark:bg-white dark:text-black dark:focus-visible:outline-white disabled:dark:bg-token-text-quaternary dark:disabled:text-token-main-surface-secondary">
|
|
105
|
+
<svg v-if="isGenerating" class="size-8 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
99
106
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
100
107
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
101
108
|
</svg>
|
|
102
|
-
<svg v-else class="size-
|
|
109
|
+
<svg v-else class="size-8" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="20" stroke-dashoffset="20" d="M12 21l0 -17.5"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.2s" values="20;0"/></path><path stroke-dasharray="12" stroke-dashoffset="12" d="M12 3l7 7M12 3l-7 7"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.2s" dur="0.2s" values="12;0"/></path></g></svg>
|
|
103
110
|
</button>
|
|
104
111
|
</div>
|
|
105
112
|
</div>
|
|
@@ -116,18 +123,19 @@ export default {
|
|
|
116
123
|
}
|
|
117
124
|
},
|
|
118
125
|
setup(props) {
|
|
126
|
+
const ai = inject('ai')
|
|
127
|
+
const chatSettings = inject('chatSettings')
|
|
119
128
|
const router = useRouter()
|
|
120
129
|
const config = inject('config')
|
|
121
130
|
const chatPrompt = inject('chatPrompt')
|
|
122
|
-
const {
|
|
123
|
-
messageText,
|
|
124
|
-
attachedFiles,
|
|
125
|
-
isGenerating,
|
|
131
|
+
const {
|
|
132
|
+
messageText,
|
|
133
|
+
attachedFiles,
|
|
134
|
+
isGenerating,
|
|
126
135
|
errorStatus,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
hasFile
|
|
136
|
+
hasImage,
|
|
137
|
+
hasAudio,
|
|
138
|
+
hasFile
|
|
131
139
|
} = chatPrompt
|
|
132
140
|
const threads = inject('threads')
|
|
133
141
|
const {
|
|
@@ -135,6 +143,8 @@ export default {
|
|
|
135
143
|
} = threads
|
|
136
144
|
|
|
137
145
|
const fileInput = ref(null)
|
|
146
|
+
const showSettings = ref(false)
|
|
147
|
+
const { applySettings } = chatSettings
|
|
138
148
|
|
|
139
149
|
// File attachments (+) handlers
|
|
140
150
|
const triggerFilePicker = () => {
|
|
@@ -185,7 +195,7 @@ export default {
|
|
|
185
195
|
if (!messageText.value.trim() || isGenerating.value || !props.model) return
|
|
186
196
|
|
|
187
197
|
// Clear any existing error message
|
|
188
|
-
errorStatus.value =
|
|
198
|
+
errorStatus.value = null
|
|
189
199
|
|
|
190
200
|
let message = messageText.value.trim()
|
|
191
201
|
if (attachedFiles.value.length) {
|
|
@@ -207,7 +217,7 @@ export default {
|
|
|
207
217
|
const newThread = await threads.createThread('New Chat', props.model, props.systemPrompt)
|
|
208
218
|
threadId = newThread.id
|
|
209
219
|
// Navigate to the new thread URL
|
|
210
|
-
router.push(
|
|
220
|
+
router.push(`${ai.base}/c/${newThread.id}`)
|
|
211
221
|
} else {
|
|
212
222
|
threadId = currentThread.value.id
|
|
213
223
|
// Update the existing thread's model and systemPrompt to match current selection
|
|
@@ -233,14 +243,19 @@ export default {
|
|
|
233
243
|
if (props.systemPrompt?.trim()) {
|
|
234
244
|
messages.unshift({
|
|
235
245
|
role: 'system',
|
|
236
|
-
content:
|
|
246
|
+
content: [
|
|
247
|
+
{ type: 'text', text: props.systemPrompt }
|
|
248
|
+
]
|
|
237
249
|
})
|
|
238
250
|
}
|
|
239
251
|
|
|
240
252
|
const chatRequest = createChatRequest()
|
|
241
253
|
chatRequest.model = props.model
|
|
242
254
|
|
|
243
|
-
|
|
255
|
+
// Apply user settings
|
|
256
|
+
applySettings(chatRequest)
|
|
257
|
+
|
|
258
|
+
console.debug('chatRequest', chatRequest, hasImage(), hasAudio(), attachedFiles.value.length, attachedFiles.value)
|
|
244
259
|
|
|
245
260
|
function setContentText(chatRequest, text) {
|
|
246
261
|
// Replace text message
|
|
@@ -256,7 +271,7 @@ export default {
|
|
|
256
271
|
if (hasImage()) {
|
|
257
272
|
const imageMessage = chatRequest.messages.find(m =>
|
|
258
273
|
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'image_url'))
|
|
259
|
-
console.
|
|
274
|
+
console.debug('hasImage', chatRequest, imageMessage)
|
|
260
275
|
if (imageMessage) {
|
|
261
276
|
const imgs = []
|
|
262
277
|
let imagePart = deepClone(imageMessage.content.find(c => c.type === 'image_url'))
|
|
@@ -272,7 +287,7 @@ export default {
|
|
|
272
287
|
}
|
|
273
288
|
|
|
274
289
|
} else if (hasAudio()) {
|
|
275
|
-
console.
|
|
290
|
+
console.debug('hasAudio', chatRequest)
|
|
276
291
|
const audioMessage = chatRequest.messages.find(m =>
|
|
277
292
|
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'input_audio'))
|
|
278
293
|
if (audioMessage) {
|
|
@@ -289,7 +304,7 @@ export default {
|
|
|
289
304
|
setContentText(chatRequest, message)
|
|
290
305
|
}
|
|
291
306
|
} else if (attachedFiles.value.length) {
|
|
292
|
-
console.
|
|
307
|
+
console.debug('hasFile', chatRequest)
|
|
293
308
|
const fileMessage = chatRequest.messages.find(m =>
|
|
294
309
|
m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'file'))
|
|
295
310
|
if (fileMessage) {
|
|
@@ -306,62 +321,80 @@ export default {
|
|
|
306
321
|
}
|
|
307
322
|
|
|
308
323
|
} else {
|
|
309
|
-
console.
|
|
324
|
+
console.debug('hasText', chatRequest)
|
|
310
325
|
// Chat template message needs to be empty
|
|
311
326
|
chatRequest.messages = []
|
|
312
327
|
messages.forEach(m => chatRequest.messages.push({
|
|
313
328
|
role: m.role,
|
|
314
|
-
content: m.content
|
|
329
|
+
content: typeof m.content === 'string'
|
|
330
|
+
? [{ type: 'text', text: m.content }]
|
|
331
|
+
: m.content
|
|
315
332
|
}))
|
|
316
333
|
}
|
|
317
334
|
|
|
318
335
|
// Send to API
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
headers: {
|
|
322
|
-
'Content-Type': 'application/json'
|
|
323
|
-
},
|
|
336
|
+
console.debug('chatRequest', chatRequest)
|
|
337
|
+
const response = await ai.post('/v1/chat/completions', {
|
|
324
338
|
body: JSON.stringify(chatRequest)
|
|
325
339
|
})
|
|
326
340
|
|
|
341
|
+
let result = null
|
|
327
342
|
if (!response.ok) {
|
|
328
|
-
errorStatus.value =
|
|
329
|
-
|
|
343
|
+
errorStatus.value = {
|
|
344
|
+
errorCode: `HTTP ${response.status} ${response.statusText}`,
|
|
345
|
+
message: null,
|
|
346
|
+
stackTrace: null
|
|
347
|
+
}
|
|
348
|
+
let errorBody = null
|
|
330
349
|
try {
|
|
331
350
|
errorBody = await response.text()
|
|
332
351
|
if (errorBody) {
|
|
333
352
|
// Try to parse as JSON for better formatting
|
|
334
353
|
try {
|
|
335
354
|
const errorJson = JSON.parse(errorBody)
|
|
336
|
-
|
|
355
|
+
const status = errorJson?.responseStatus
|
|
356
|
+
if (status) {
|
|
357
|
+
errorStatus.value.errorCode += ` ${status.errorCode}`
|
|
358
|
+
errorStatus.value.message = status.message
|
|
359
|
+
errorStatus.value.stackTrace = status.stackTrace
|
|
360
|
+
} else {
|
|
361
|
+
errorStatus.value.stackTrace = JSON.stringify(errorJson, null, 2)
|
|
362
|
+
}
|
|
337
363
|
} catch (e) {
|
|
338
364
|
}
|
|
339
365
|
}
|
|
340
366
|
} catch (e) {
|
|
341
367
|
// If we can't read the response body, just use the status
|
|
342
368
|
}
|
|
343
|
-
|
|
369
|
+
} else {
|
|
370
|
+
try {
|
|
371
|
+
result = await response.json()
|
|
372
|
+
} catch (e) {
|
|
373
|
+
errorStatus.value = {
|
|
374
|
+
errorCode: 'Error',
|
|
375
|
+
message: e.message,
|
|
376
|
+
stackTrace: null
|
|
377
|
+
}
|
|
378
|
+
}
|
|
344
379
|
}
|
|
345
380
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
381
|
+
if (result?.error) {
|
|
382
|
+
errorStatus.value ??= {
|
|
383
|
+
errorCode: 'Error',
|
|
384
|
+
}
|
|
385
|
+
errorStatus.value.message = result.error
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!errorStatus.value) {
|
|
389
|
+
// Add assistant response (save entire message including reasoning)
|
|
390
|
+
const assistantMessage = result.choices?.[0]?.message
|
|
391
|
+
await threads.addMessageToThread(threadId, assistantMessage)
|
|
392
|
+
|
|
393
|
+
nextTick(addCopyButtons)
|
|
394
|
+
|
|
395
|
+
attachedFiles.value = []
|
|
396
|
+
// Error will be cleared when user sends next message (no auto-timeout)
|
|
350
397
|
}
|
|
351
|
-
|
|
352
|
-
// Add assistant response (save entire message including reasoning)
|
|
353
|
-
const assistantMessage = result.choices?.[0]?.message
|
|
354
|
-
await threads.addMessageToThread(threadId, assistantMessage)
|
|
355
|
-
|
|
356
|
-
nextTick(addCopyButtons)
|
|
357
|
-
|
|
358
|
-
attachedFiles.value = []
|
|
359
|
-
|
|
360
|
-
} catch (error) {
|
|
361
|
-
console.error('Error sending message:', error)
|
|
362
|
-
errorMessage.value = error.message
|
|
363
|
-
|
|
364
|
-
// Error will be cleared when user sends next message (no auto-timeout)
|
|
365
398
|
} finally {
|
|
366
399
|
isGenerating.value = false
|
|
367
400
|
}
|
|
@@ -375,10 +408,9 @@ export default {
|
|
|
375
408
|
return {
|
|
376
409
|
isGenerating,
|
|
377
410
|
attachedFiles,
|
|
378
|
-
errorStatus,
|
|
379
|
-
errorMessage,
|
|
380
411
|
messageText,
|
|
381
412
|
fileInput,
|
|
413
|
+
showSettings,
|
|
382
414
|
triggerFilePicker,
|
|
383
415
|
onFilesSelected,
|
|
384
416
|
removeAttachment,
|