llms-py 2.0.20__py3-none-any.whl → 3.0.10__py3-none-any.whl
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/__init__.py +3 -1
- llms/db.py +359 -0
- llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +254 -327
- llms/extensions/app/README.md +20 -0
- llms/extensions/app/__init__.py +589 -0
- llms/extensions/app/db.py +536 -0
- llms/{ui → extensions/app/ui}/Recents.mjs +99 -73
- llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +139 -68
- llms/extensions/app/ui/threadStore.mjs +433 -0
- llms/extensions/core_tools/CALCULATOR.md +32 -0
- llms/extensions/core_tools/__init__.py +637 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
- llms/extensions/core_tools/ui/index.mjs +650 -0
- llms/extensions/gallery/README.md +61 -0
- llms/extensions/gallery/__init__.py +63 -0
- llms/extensions/gallery/db.py +243 -0
- llms/extensions/gallery/ui/index.mjs +482 -0
- llms/extensions/katex/README.md +39 -0
- llms/extensions/katex/__init__.py +6 -0
- llms/extensions/katex/ui/README.md +125 -0
- llms/extensions/katex/ui/contrib/auto-render.js +338 -0
- llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
- llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
- llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
- llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
- llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
- llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
- llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
- llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- llms/extensions/katex/ui/index.mjs +92 -0
- llms/extensions/katex/ui/katex-swap.css +1230 -0
- llms/extensions/katex/ui/katex-swap.min.css +1 -0
- llms/extensions/katex/ui/katex.css +1230 -0
- llms/extensions/katex/ui/katex.js +19080 -0
- llms/extensions/katex/ui/katex.min.css +1 -0
- llms/extensions/katex/ui/katex.min.js +1 -0
- llms/extensions/katex/ui/katex.min.mjs +1 -0
- llms/extensions/katex/ui/katex.mjs +18547 -0
- llms/extensions/providers/__init__.py +22 -0
- llms/extensions/providers/anthropic.py +233 -0
- llms/extensions/providers/cerebras.py +37 -0
- llms/extensions/providers/chutes.py +153 -0
- llms/extensions/providers/google.py +481 -0
- llms/extensions/providers/nvidia.py +103 -0
- llms/extensions/providers/openai.py +154 -0
- llms/extensions/providers/openrouter.py +74 -0
- llms/extensions/providers/zai.py +182 -0
- llms/extensions/system_prompts/README.md +22 -0
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/ui/index.mjs +280 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +144 -0
- llms/extensions/tools/ui/index.mjs +706 -0
- llms/index.html +36 -62
- llms/llms.json +180 -879
- llms/main.py +3640 -899
- llms/providers-extra.json +394 -0
- llms/providers.json +1 -0
- llms/ui/App.mjs +176 -8
- llms/ui/ai.mjs +156 -20
- llms/ui/app.css +3161 -244
- llms/ui/ctx.mjs +412 -0
- llms/ui/index.mjs +131 -0
- llms/ui/lib/chart.js +14 -0
- llms/ui/lib/charts.mjs +16 -0
- llms/ui/lib/color.js +14 -0
- llms/ui/lib/highlight.min.mjs +1243 -0
- llms/ui/lib/idb.min.mjs +8 -0
- llms/ui/lib/marked.min.mjs +8 -0
- llms/ui/lib/servicestack-client.mjs +1 -0
- llms/ui/lib/servicestack-vue.mjs +37 -0
- llms/ui/lib/vue-router.min.mjs +6 -0
- llms/ui/lib/vue.min.mjs +13 -0
- llms/ui/lib/vue.mjs +18530 -0
- llms/ui/markdown.mjs +25 -14
- llms/ui/modules/chat/ChatBody.mjs +976 -0
- llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
- llms/ui/modules/chat/index.mjs +991 -0
- llms/ui/modules/icons.mjs +46 -0
- llms/ui/modules/layout.mjs +271 -0
- llms/ui/modules/model-selector.mjs +811 -0
- llms/ui/tailwind.input.css +550 -78
- llms/ui/typography.css +54 -36
- llms/ui/utils.mjs +197 -92
- llms_py-3.0.10.dist-info/METADATA +49 -0
- llms_py-3.0.10.dist-info/RECORD +177 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/licenses/LICENSE +1 -2
- llms/ui/Avatar.mjs +0 -28
- llms/ui/Brand.mjs +0 -34
- llms/ui/ChatPrompt.mjs +0 -443
- llms/ui/Main.mjs +0 -740
- llms/ui/ModelSelector.mjs +0 -60
- llms/ui/ProviderIcon.mjs +0 -29
- llms/ui/ProviderStatus.mjs +0 -105
- llms/ui/SignIn.mjs +0 -64
- llms/ui/SystemPromptEditor.mjs +0 -31
- llms/ui/SystemPromptSelector.mjs +0 -36
- llms/ui/Welcome.mjs +0 -8
- llms/ui/threadStore.mjs +0 -524
- llms/ui.json +0 -1069
- llms_py-2.0.20.dist-info/METADATA +0 -931
- llms_py-2.0.20.dist-info/RECORD +0 -36
- {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/WHEEL +0 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.10.dist-info}/top_level.txt +0 -0
llms/ui/Main.mjs
DELETED
|
@@ -1,740 +0,0 @@
|
|
|
1
|
-
import { ref, computed, nextTick, watch, onMounted, provide, inject } from 'vue'
|
|
2
|
-
import { useRouter, useRoute } from 'vue-router'
|
|
3
|
-
import { useFormatters } from '@servicestack/vue'
|
|
4
|
-
import { useThreadStore } from './threadStore.mjs'
|
|
5
|
-
import { storageObject, addCopyButtons, formatCost, statsTitle } from './utils.mjs'
|
|
6
|
-
import { renderMarkdown } from './markdown.mjs'
|
|
7
|
-
import ChatPrompt, { useChatPrompt } from './ChatPrompt.mjs'
|
|
8
|
-
import SignIn from './SignIn.mjs'
|
|
9
|
-
import Avatar from './Avatar.mjs'
|
|
10
|
-
import ModelSelector from './ModelSelector.mjs'
|
|
11
|
-
import SystemPromptSelector from './SystemPromptSelector.mjs'
|
|
12
|
-
import SystemPromptEditor from './SystemPromptEditor.mjs'
|
|
13
|
-
import { useSettings } from "./SettingsDialog.mjs"
|
|
14
|
-
import Welcome from './Welcome.mjs'
|
|
15
|
-
|
|
16
|
-
const { humanifyMs, humanifyNumber } = useFormatters()
|
|
17
|
-
|
|
18
|
-
export default {
|
|
19
|
-
components: {
|
|
20
|
-
ModelSelector,
|
|
21
|
-
SystemPromptSelector,
|
|
22
|
-
SystemPromptEditor,
|
|
23
|
-
ChatPrompt,
|
|
24
|
-
SignIn,
|
|
25
|
-
Avatar,
|
|
26
|
-
Welcome,
|
|
27
|
-
},
|
|
28
|
-
template: `
|
|
29
|
-
<div class="flex flex-col h-full w-full">
|
|
30
|
-
<!-- Header with model and prompt selectors -->
|
|
31
|
-
<div class="border-b border-gray-200 bg-white px-2 py-2 w-full min-h-16">
|
|
32
|
-
<div class="flex items-center justify-between w-full">
|
|
33
|
-
<ModelSelector :models="models" v-model="selectedModel" @updated="configUpdated" />
|
|
34
|
-
|
|
35
|
-
<div class="flex items-center space-x-2">
|
|
36
|
-
<SystemPromptSelector :prompts="prompts" v-model="selectedPrompt"
|
|
37
|
-
:show="showSystemPrompt" @toggle="showSystemPrompt = !showSystemPrompt" />
|
|
38
|
-
<Avatar />
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
<SystemPromptEditor v-if="showSystemPrompt"
|
|
44
|
-
v-model="currentSystemPrompt" :prompts="prompts" :selected="selectedPrompt" />
|
|
45
|
-
|
|
46
|
-
<!-- Messages Area -->
|
|
47
|
-
<div class="flex-1 overflow-y-auto" ref="messagesContainer">
|
|
48
|
-
<div class="mx-auto max-w-6xl px-4 py-6">
|
|
49
|
-
<div v-if="$ai.requiresAuth && !$ai.auth">
|
|
50
|
-
<SignIn @done="$ai.signIn($event)" />
|
|
51
|
-
</div>
|
|
52
|
-
<!-- Welcome message when no thread is selected -->
|
|
53
|
-
<div v-else-if="!currentThread" class="text-center py-12">
|
|
54
|
-
<Welcome />
|
|
55
|
-
|
|
56
|
-
<!-- Chat input for new conversation -->
|
|
57
|
-
<div class="max-w-2xl mx-auto">
|
|
58
|
-
<ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<!-- Export/Import buttons -->
|
|
62
|
-
<div class="mt-2 flex space-x-3 justify-center">
|
|
63
|
-
<button type="button"
|
|
64
|
-
@click="exportThreads"
|
|
65
|
-
:disabled="isExporting"
|
|
66
|
-
:title="'Export ' + threads?.threads?.value?.length + ' conversations'"
|
|
67
|
-
class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
68
|
-
>
|
|
69
|
-
<svg v-if="!isExporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
70
|
-
<path fill="currentColor" d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z"></path>
|
|
71
|
-
</svg>
|
|
72
|
-
<svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
73
|
-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
74
|
-
<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>
|
|
75
|
-
</svg>
|
|
76
|
-
{{ isExporting ? 'Exporting...' : 'Export' }}
|
|
77
|
-
</button>
|
|
78
|
-
|
|
79
|
-
<button type="button"
|
|
80
|
-
@click="triggerImport"
|
|
81
|
-
:disabled="isImporting"
|
|
82
|
-
title="Import conversations from JSON file"
|
|
83
|
-
class="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
84
|
-
>
|
|
85
|
-
<svg v-if="!isImporting" class="size-5 mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
86
|
-
<path fill="currentColor" d="m14 12l-4-4v3H2v2h8v3m10 2V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v3h2V6h12v12H6v-3H4v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2"/>
|
|
87
|
-
</svg>
|
|
88
|
-
<svg v-else class="size-5 mr-1 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
89
|
-
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
90
|
-
<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>
|
|
91
|
-
</svg>
|
|
92
|
-
{{ isImporting ? 'Importing...' : 'Import' }}
|
|
93
|
-
</button>
|
|
94
|
-
|
|
95
|
-
<!-- Hidden file input for import -->
|
|
96
|
-
<input
|
|
97
|
-
ref="fileInput"
|
|
98
|
-
type="file"
|
|
99
|
-
accept=".json"
|
|
100
|
-
@change="handleFileImport"
|
|
101
|
-
class="hidden"
|
|
102
|
-
/>
|
|
103
|
-
</div>
|
|
104
|
-
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<!-- Messages -->
|
|
108
|
-
<div v-else class="space-y-6">
|
|
109
|
-
<div
|
|
110
|
-
v-for="message in currentThread.messages"
|
|
111
|
-
:key="message.id"
|
|
112
|
-
class="flex items-start space-x-3 group"
|
|
113
|
-
:class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
|
|
114
|
-
>
|
|
115
|
-
<!-- Avatar outside the bubble -->
|
|
116
|
-
<div class="flex-shrink-0 flex flex-col justify-center">
|
|
117
|
-
<div class="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium"
|
|
118
|
-
:class="message.role === 'user'
|
|
119
|
-
? 'bg-blue-600 text-white'
|
|
120
|
-
: 'bg-gray-600 text-white'"
|
|
121
|
-
>
|
|
122
|
-
{{ message.role === 'user' ? 'U' : 'AI' }}
|
|
123
|
-
</div>
|
|
124
|
-
|
|
125
|
-
<!-- Delete button (shown on hover) -->
|
|
126
|
-
<button type="button" @click.stop="threads.deleteMessageFromThread(currentThread.id, message.id)"
|
|
127
|
-
class="mx-auto opacity-0 group-hover:opacity-100 mt-2 rounded text-gray-400 hover:text-red-600 hover:bg-red-50 transition-all"
|
|
128
|
-
title="Delete message">
|
|
129
|
-
<svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
130
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
131
|
-
</svg>
|
|
132
|
-
</button>
|
|
133
|
-
</div>
|
|
134
|
-
|
|
135
|
-
<!-- Message bubble -->
|
|
136
|
-
<div
|
|
137
|
-
class="message rounded-lg px-4 py-3 relative group"
|
|
138
|
-
:class="message.role === 'user'
|
|
139
|
-
? 'bg-blue-600 text-white'
|
|
140
|
-
: 'bg-gray-100 text-gray-900 border border-gray-200'"
|
|
141
|
-
>
|
|
142
|
-
<!-- Copy button in top right corner -->
|
|
143
|
-
<button
|
|
144
|
-
type="button"
|
|
145
|
-
@click="copyMessageContent(message)"
|
|
146
|
-
class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 p-1 rounded hover:bg-black/10 focus:outline-none focus:ring-0"
|
|
147
|
-
:class="message.role === 'user' ? 'text-white/70 hover:text-white hover:bg-white/20' : 'text-gray-500 hover:text-gray-700'"
|
|
148
|
-
title="Copy message content"
|
|
149
|
-
>
|
|
150
|
-
<svg v-if="copying === message" class="size-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
|
|
151
|
-
<svg v-else class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
152
|
-
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
|
|
153
|
-
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
|
|
154
|
-
</svg>
|
|
155
|
-
</button>
|
|
156
|
-
|
|
157
|
-
<div
|
|
158
|
-
v-if="message.role === 'assistant'"
|
|
159
|
-
v-html="renderMarkdown(message.content)"
|
|
160
|
-
class="prose prose-sm max-w-none"
|
|
161
|
-
></div>
|
|
162
|
-
|
|
163
|
-
<!-- Collapsible reasoning section -->
|
|
164
|
-
<div v-if="message.role === 'assistant' && message.reasoning" class="mt-2">
|
|
165
|
-
<button type="button" @click="toggleReasoning(message.id)" class="text-xs text-gray-600 hover:text-gray-800 flex items-center space-x-1">
|
|
166
|
-
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" :class="isReasoningExpanded(message.id) ? 'transform rotate-90' : ''"><path fill="currentColor" d="M7 5l6 5l-6 5z"/></svg>
|
|
167
|
-
<span>{{ isReasoningExpanded(message.id) ? 'Hide reasoning' : 'Show reasoning' }}</span>
|
|
168
|
-
</button>
|
|
169
|
-
<div v-if="isReasoningExpanded(message.id)" class="mt-2 rounded border border-gray-200 bg-gray-50 p-2">
|
|
170
|
-
<div v-if="typeof message.reasoning === 'string'" v-html="renderMarkdown(message.reasoning)" class="prose prose-xs max-w-none"></div>
|
|
171
|
-
<pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto">{{ formatReasoning(message.reasoning) }}</pre>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
|
|
175
|
-
<div v-if="message.role !== 'assistant'" class="whitespace-pre-wrap">{{ message.content }}</div>
|
|
176
|
-
<div class="mt-2 text-xs opacity-70">
|
|
177
|
-
<span>{{ formatTime(message.timestamp) }}</span>
|
|
178
|
-
<span v-if="message.usage" :title="tokensTitle(message.usage)">
|
|
179
|
-
•
|
|
180
|
-
{{ humanifyNumber(message.usage.tokens) }} tokens
|
|
181
|
-
<span v-if="message.usage.cost">· {{ message.usage.cost }}</span>
|
|
182
|
-
<span v-if="message.usage.duration"> in {{ humanifyMs(message.usage.duration) }}</span>
|
|
183
|
-
</span>
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
<!-- Edit and Redo buttons (shown on hover for user messages, outside bubble) -->
|
|
188
|
-
<div v-if="message.role === 'user'" class="flex flex-col gap-2 opacity-0 group-hover:opacity-100 transition-opacity mt-1">
|
|
189
|
-
<button type="button" @click.stop="editMessage(message)"
|
|
190
|
-
class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 hover:text-green-600 hover:bg-green-50 transition-all"
|
|
191
|
-
title="Edit message">
|
|
192
|
-
<svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
193
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
194
|
-
</svg>
|
|
195
|
-
Edit
|
|
196
|
-
</button>
|
|
197
|
-
<button type="button" @click.stop="redoMessage(message)"
|
|
198
|
-
class="whitespace-nowrap text-xs px-2 py-1 rounded text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-all"
|
|
199
|
-
title="Redo message (clears all responses after this message and re-runs it)">
|
|
200
|
-
<svg class="size-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
201
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
202
|
-
</svg>
|
|
203
|
-
Redo
|
|
204
|
-
</button>
|
|
205
|
-
</div>
|
|
206
|
-
</div>
|
|
207
|
-
|
|
208
|
-
<div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 text-sm">
|
|
209
|
-
<span :title="statsTitle(currentThread.stats)">
|
|
210
|
-
{{ currentThread.stats.cost ? formatCost(currentThread.stats.cost) + ' for ' : '' }} {{ humanifyNumber(currentThread.stats.inputTokens) }} → {{ humanifyNumber(currentThread.stats.outputTokens) }} tokens over {{ currentThread.stats.requests }} request{{currentThread.stats.requests===1?'':'s'}} in {{ humanifyMs(currentThread.stats.duration) }}
|
|
211
|
-
</span>
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
<!-- Loading indicator -->
|
|
215
|
-
<div v-if="isGenerating" class="flex items-start space-x-3">
|
|
216
|
-
<!-- Avatar outside the bubble -->
|
|
217
|
-
<div class="flex-shrink-0">
|
|
218
|
-
<div class="w-8 h-8 rounded-full bg-gray-600 text-white flex items-center justify-center text-sm font-medium">
|
|
219
|
-
AI
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
|
|
223
|
-
<!-- Loading bubble -->
|
|
224
|
-
<div class="rounded-lg px-4 py-3 bg-gray-100 border border-gray-200">
|
|
225
|
-
<div class="flex space-x-1">
|
|
226
|
-
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
|
227
|
-
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
|
|
228
|
-
<div class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
<!-- Error message bubble -->
|
|
234
|
-
<div v-if="errorStatus" class="flex items-start space-x-3">
|
|
235
|
-
<!-- Avatar outside the bubble -->
|
|
236
|
-
<div class="flex-shrink-0">
|
|
237
|
-
<div class="w-8 h-8 rounded-full bg-red-600 text-white flex items-center justify-center text-sm font-medium">
|
|
238
|
-
!
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
|
|
242
|
-
<!-- Error bubble -->
|
|
243
|
-
<div class="max-w-[85%] rounded-lg px-4 py-3 bg-red-50 border border-red-200 text-red-800 shadow-sm">
|
|
244
|
-
<div class="flex items-start space-x-2">
|
|
245
|
-
<div class="flex-1 min-w-0">
|
|
246
|
-
<div class="text-base font-medium mb-1">{{ errorStatus?.errorCode || 'Error' }}</div>
|
|
247
|
-
<div v-if="errorStatus?.message" class="text-base mb-1">{{ errorStatus.message }}</div>
|
|
248
|
-
<div v-if="errorStatus?.stackTrace" class="text-sm whitespace-pre-wrap break-words max-h-80 overflow-y-auto font-mono p-2 rounded">
|
|
249
|
-
{{ errorStatus.stackTrace }}
|
|
250
|
-
</div>
|
|
251
|
-
</div>
|
|
252
|
-
<button type="button"
|
|
253
|
-
@click="errorStatus = null"
|
|
254
|
-
class="text-red-400 hover:text-red-600 flex-shrink-0"
|
|
255
|
-
>
|
|
256
|
-
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
257
|
-
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
258
|
-
</svg>
|
|
259
|
-
</button>
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
|
|
266
|
-
<!-- Edit message modal -->
|
|
267
|
-
<div v-if="editingMessageId" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
268
|
-
<div class="relative bg-white rounded-lg shadow-lg p-6 max-w-2xl w-full mx-4">
|
|
269
|
-
<CloseButton @click="cancelEdit" class="" />
|
|
270
|
-
<h3 class="text-lg font-semibold text-gray-900 mb-4">Edit Message</h3>
|
|
271
|
-
<textarea
|
|
272
|
-
v-model="editingMessageContent"
|
|
273
|
-
class="w-full h-40 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
|
274
|
-
placeholder="Edit your message..."
|
|
275
|
-
></textarea>
|
|
276
|
-
<div class="mt-4 flex gap-2 justify-end">
|
|
277
|
-
<button type="button" @click="cancelEdit"
|
|
278
|
-
class="px-4 py-2 rounded-md border border-gray-300 text-gray-700 hover:bg-gray-50 transition-all">
|
|
279
|
-
Cancel
|
|
280
|
-
</button>
|
|
281
|
-
<button type="button" @click="saveEditedMessage"
|
|
282
|
-
class="px-4 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-all">
|
|
283
|
-
Save
|
|
284
|
-
</button>
|
|
285
|
-
</div>
|
|
286
|
-
</div>
|
|
287
|
-
</div>
|
|
288
|
-
</div>
|
|
289
|
-
|
|
290
|
-
<!-- Input Area - only show when thread is selected -->
|
|
291
|
-
<div v-if="currentThread" class="flex-shrink-0 border-t border-gray-200 bg-white px-6 py-4">
|
|
292
|
-
<ChatPrompt :model="selectedModel" :systemPrompt="currentSystemPrompt" />
|
|
293
|
-
</div>
|
|
294
|
-
</div>
|
|
295
|
-
`,
|
|
296
|
-
props: {
|
|
297
|
-
},
|
|
298
|
-
setup(props) {
|
|
299
|
-
const ai = inject('ai')
|
|
300
|
-
const router = useRouter()
|
|
301
|
-
const route = useRoute()
|
|
302
|
-
const threads = useThreadStore()
|
|
303
|
-
const { currentThread } = threads
|
|
304
|
-
const chatPrompt = useChatPrompt()
|
|
305
|
-
const chatSettings = useSettings()
|
|
306
|
-
const {
|
|
307
|
-
errorStatus,
|
|
308
|
-
isGenerating,
|
|
309
|
-
} = chatPrompt
|
|
310
|
-
provide('threads', threads)
|
|
311
|
-
provide('chatPrompt', chatPrompt)
|
|
312
|
-
provide('chatSettings', chatSettings)
|
|
313
|
-
const models = inject('models')
|
|
314
|
-
const config = inject('config')
|
|
315
|
-
|
|
316
|
-
const prefs = storageObject(ai.prefsKey)
|
|
317
|
-
|
|
318
|
-
const customPromptValue = ref('')
|
|
319
|
-
const customPrompt = {
|
|
320
|
-
id: '_custom_',
|
|
321
|
-
name: 'Custom...',
|
|
322
|
-
value: ''
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const prompts = computed(() => [customPrompt, ...config.prompts])
|
|
326
|
-
|
|
327
|
-
const selectedModel = ref(prefs.model || config.defaults.text.model || '')
|
|
328
|
-
const selectedPrompt = ref(prefs.systemPrompt || null)
|
|
329
|
-
const currentSystemPrompt = ref('')
|
|
330
|
-
const showSystemPrompt = ref(false)
|
|
331
|
-
const messagesContainer = ref(null)
|
|
332
|
-
const isExporting = ref(false)
|
|
333
|
-
const isImporting = ref(false)
|
|
334
|
-
const fileInput = ref(null)
|
|
335
|
-
const copying = ref(null)
|
|
336
|
-
const editingMessageId = ref(null)
|
|
337
|
-
const editingMessageContent = ref('')
|
|
338
|
-
const editingMessage = ref(null)
|
|
339
|
-
|
|
340
|
-
// Auto-scroll to bottom when new messages arrive
|
|
341
|
-
const scrollToBottom = async () => {
|
|
342
|
-
await nextTick()
|
|
343
|
-
if (messagesContainer.value) {
|
|
344
|
-
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Watch for new messages and scroll
|
|
349
|
-
watch(() => currentThread.value?.messages?.length, scrollToBottom)
|
|
350
|
-
|
|
351
|
-
// Watch for route changes and load the appropriate thread
|
|
352
|
-
watch(() => route.params.id, async (newId) => {
|
|
353
|
-
const thread = await threads.setCurrentThreadFromRoute(newId, router)
|
|
354
|
-
|
|
355
|
-
// If the selected thread specifies a model and it's available, switch to it
|
|
356
|
-
if (thread?.model && Array.isArray(models) && models.includes(thread.model)) {
|
|
357
|
-
selectedModel.value = thread.model
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Sync System Prompt selection from thread
|
|
361
|
-
if (thread) {
|
|
362
|
-
const norm = s => (s || '').replace(/\s+/g, ' ').trim()
|
|
363
|
-
const tsp = norm(thread.systemPrompt || '')
|
|
364
|
-
if (tsp) {
|
|
365
|
-
const match = config.prompts.find(p => norm(p.value) === tsp)
|
|
366
|
-
if (match) {
|
|
367
|
-
selectedPrompt.value = match
|
|
368
|
-
currentSystemPrompt.value = match.value.replace(/\n/g, ' ')
|
|
369
|
-
} else {
|
|
370
|
-
selectedPrompt.value = customPrompt
|
|
371
|
-
currentSystemPrompt.value = thread.systemPrompt
|
|
372
|
-
}
|
|
373
|
-
} else {
|
|
374
|
-
// Preserve existing selected prompt
|
|
375
|
-
// selectedPrompt.value = null
|
|
376
|
-
// currentSystemPrompt.value = ''
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (!newId) {
|
|
381
|
-
chatPrompt.reset()
|
|
382
|
-
}
|
|
383
|
-
nextTick(addCopyButtons)
|
|
384
|
-
}, { immediate: true })
|
|
385
|
-
|
|
386
|
-
// Watch selectedPrompt and update currentSystemPrompt
|
|
387
|
-
watch(selectedPrompt, (newPrompt) => {
|
|
388
|
-
// If using a custom prompt, keep whatever is already in currentSystemPrompt
|
|
389
|
-
if (newPrompt && newPrompt.id === '_custom_') return
|
|
390
|
-
const prompt = newPrompt && config.prompts.find(p => p.id === newPrompt.id)
|
|
391
|
-
currentSystemPrompt.value = prompt ? prompt.value.replace(/\n/g,' ') : ''
|
|
392
|
-
}, { immediate: true })
|
|
393
|
-
|
|
394
|
-
watch(() => [selectedModel.value, selectedPrompt.value], () => {
|
|
395
|
-
localStorage.setItem(ai.prefsKey, JSON.stringify({
|
|
396
|
-
model: selectedModel.value,
|
|
397
|
-
systemPrompt: selectedPrompt.value
|
|
398
|
-
}))
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
async function exportThreads() {
|
|
402
|
-
if (isExporting.value) return
|
|
403
|
-
|
|
404
|
-
isExporting.value = true
|
|
405
|
-
try {
|
|
406
|
-
// Load all threads from IndexedDB
|
|
407
|
-
await threads.loadThreads()
|
|
408
|
-
const allThreads = threads.threads.value
|
|
409
|
-
|
|
410
|
-
// Create export data with metadata
|
|
411
|
-
const exportData = {
|
|
412
|
-
exportedAt: new Date().toISOString(),
|
|
413
|
-
version: '1.0',
|
|
414
|
-
source: 'ServiceStack.AI.Chat',
|
|
415
|
-
threadCount: allThreads.length,
|
|
416
|
-
threads: allThreads
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Create and download JSON file
|
|
420
|
-
const jsonString = JSON.stringify(exportData, null, 2)
|
|
421
|
-
const blob = new Blob([jsonString], { type: 'application/json' })
|
|
422
|
-
const url = URL.createObjectURL(blob)
|
|
423
|
-
|
|
424
|
-
const link = document.createElement('a')
|
|
425
|
-
link.href = url
|
|
426
|
-
link.download = `aichat-threads-export-${new Date().toISOString().split('T')[0]}.json`
|
|
427
|
-
document.body.appendChild(link)
|
|
428
|
-
link.click()
|
|
429
|
-
document.body.removeChild(link)
|
|
430
|
-
URL.revokeObjectURL(url)
|
|
431
|
-
|
|
432
|
-
} catch (error) {
|
|
433
|
-
console.error('Failed to export threads:', error)
|
|
434
|
-
alert('Failed to export threads: ' + error.message)
|
|
435
|
-
} finally {
|
|
436
|
-
isExporting.value = false
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function triggerImport() {
|
|
441
|
-
if (isImporting.value) return
|
|
442
|
-
fileInput.value?.click()
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
async function handleFileImport(event) {
|
|
446
|
-
const file = event.target.files?.[0]
|
|
447
|
-
if (!file) return
|
|
448
|
-
|
|
449
|
-
isImporting.value = true
|
|
450
|
-
try {
|
|
451
|
-
const text = await file.text()
|
|
452
|
-
const importData = JSON.parse(text)
|
|
453
|
-
|
|
454
|
-
// Validate import data structure
|
|
455
|
-
if (!importData.threads || !Array.isArray(importData.threads)) {
|
|
456
|
-
throw new Error('Invalid import file: missing or invalid threads array')
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Import threads one by one
|
|
460
|
-
let importedCount = 0
|
|
461
|
-
let updatedCount = 0
|
|
462
|
-
|
|
463
|
-
for (const threadData of importData.threads) {
|
|
464
|
-
if (!threadData.id) {
|
|
465
|
-
console.warn('Skipping thread without ID:', threadData)
|
|
466
|
-
continue
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
// Check if thread already exists
|
|
471
|
-
const existingThread = await threads.getThread(threadData.id)
|
|
472
|
-
|
|
473
|
-
if (existingThread) {
|
|
474
|
-
// Update existing thread
|
|
475
|
-
await threads.updateThread(threadData.id, {
|
|
476
|
-
title: threadData.title,
|
|
477
|
-
model: threadData.model,
|
|
478
|
-
systemPrompt: threadData.systemPrompt,
|
|
479
|
-
messages: threadData.messages || [],
|
|
480
|
-
createdAt: threadData.createdAt,
|
|
481
|
-
// Keep the existing updatedAt or use imported one
|
|
482
|
-
updatedAt: threadData.updatedAt || existingThread.updatedAt
|
|
483
|
-
})
|
|
484
|
-
updatedCount++
|
|
485
|
-
} else {
|
|
486
|
-
// Add new thread directly to IndexedDB
|
|
487
|
-
//await threads.initDB()
|
|
488
|
-
const db = await threads.initDB()
|
|
489
|
-
const tx = db.transaction(['threads'], 'readwrite')
|
|
490
|
-
await tx.objectStore('threads').add({
|
|
491
|
-
id: threadData.id,
|
|
492
|
-
title: threadData.title || 'Imported Chat',
|
|
493
|
-
model: threadData.model || '',
|
|
494
|
-
systemPrompt: threadData.systemPrompt || '',
|
|
495
|
-
messages: threadData.messages || [],
|
|
496
|
-
createdAt: threadData.createdAt || new Date().toISOString(),
|
|
497
|
-
updatedAt: threadData.updatedAt || new Date().toISOString()
|
|
498
|
-
})
|
|
499
|
-
await tx.complete
|
|
500
|
-
importedCount++
|
|
501
|
-
}
|
|
502
|
-
} catch (error) {
|
|
503
|
-
console.error('Failed to import thread:', threadData.id, error)
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Reload threads to reflect changes
|
|
508
|
-
await threads.loadThreads()
|
|
509
|
-
|
|
510
|
-
alert(`Import completed!\nNew threads: ${importedCount}\nUpdated threads: ${updatedCount}`)
|
|
511
|
-
|
|
512
|
-
} catch (error) {
|
|
513
|
-
console.error('Failed to import threads:', error)
|
|
514
|
-
alert('Failed to import threads: ' + error.message)
|
|
515
|
-
} finally {
|
|
516
|
-
isImporting.value = false
|
|
517
|
-
// Clear the file input
|
|
518
|
-
if (fileInput.value) {
|
|
519
|
-
fileInput.value.value = ''
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function configUpdated() {
|
|
525
|
-
console.log('configUpdated', selectedModel.value, models.length, models.includes(selectedModel.value))
|
|
526
|
-
if (selectedModel.value && !models.includes(selectedModel.value)) {
|
|
527
|
-
selectedModel.value = config.defaults.text.model || ''
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Format timestamp
|
|
532
|
-
const formatTime = (timestamp) => {
|
|
533
|
-
return new Date(timestamp).toLocaleTimeString([], {
|
|
534
|
-
hour: '2-digit',
|
|
535
|
-
minute: '2-digit'
|
|
536
|
-
})
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Reasoning collapse state and helpers
|
|
540
|
-
const expandedReasoning = ref(new Set())
|
|
541
|
-
const isReasoningExpanded = (id) => expandedReasoning.value.has(id)
|
|
542
|
-
const toggleReasoning = (id) => {
|
|
543
|
-
const s = new Set(expandedReasoning.value)
|
|
544
|
-
if (s.has(id)) {
|
|
545
|
-
s.delete(id)
|
|
546
|
-
} else {
|
|
547
|
-
s.add(id)
|
|
548
|
-
}
|
|
549
|
-
expandedReasoning.value = s
|
|
550
|
-
}
|
|
551
|
-
const formatReasoning = (r) => typeof r === 'string' ? r : JSON.stringify(r, null, 2)
|
|
552
|
-
|
|
553
|
-
// Copy message content to clipboard
|
|
554
|
-
const copyMessageContent = async (message) => {
|
|
555
|
-
try {
|
|
556
|
-
copying.value = message
|
|
557
|
-
await navigator.clipboard.writeText(message.content)
|
|
558
|
-
// Could add a toast notification here if desired
|
|
559
|
-
} catch (err) {
|
|
560
|
-
console.error('Failed to copy message content:', err)
|
|
561
|
-
// Fallback for older browsers
|
|
562
|
-
const textArea = document.createElement('textarea')
|
|
563
|
-
textArea.value = message.content
|
|
564
|
-
document.body.appendChild(textArea)
|
|
565
|
-
textArea.select()
|
|
566
|
-
document.execCommand('copy')
|
|
567
|
-
document.body.removeChild(textArea)
|
|
568
|
-
}
|
|
569
|
-
setTimeout(() => { copying.value = null }, 2000)
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Redo a user message (clear all messages after it and re-run)
|
|
573
|
-
const redoMessage = async (message) => {
|
|
574
|
-
if (!currentThread.value || message.role !== 'user') return
|
|
575
|
-
|
|
576
|
-
try {
|
|
577
|
-
const threadId = currentThread.value.id
|
|
578
|
-
|
|
579
|
-
// Clear all messages after this one
|
|
580
|
-
await threads.redoMessageFromThread(threadId, message.id)
|
|
581
|
-
|
|
582
|
-
// Extract the actual message content (remove media indicators if present)
|
|
583
|
-
let messageContent = message.content
|
|
584
|
-
// Remove media indicators like [🖼️ filename] or [🔉 filename] or [📎 filename]
|
|
585
|
-
messageContent = messageContent.replace(/\n\n\[[🖼️🔉📎] [^\]]+\]$/, '')
|
|
586
|
-
|
|
587
|
-
// Set the message text in the chat prompt
|
|
588
|
-
chatPrompt.messageText.value = messageContent
|
|
589
|
-
|
|
590
|
-
// Clear any attached files since we're re-running
|
|
591
|
-
chatPrompt.attachedFiles.value = []
|
|
592
|
-
|
|
593
|
-
// Trigger send by simulating the send action
|
|
594
|
-
// We'll use a small delay to ensure the UI updates
|
|
595
|
-
await nextTick()
|
|
596
|
-
|
|
597
|
-
// Find the send button and click it
|
|
598
|
-
const sendButton = document.querySelector('button[title*="Send"]')
|
|
599
|
-
if (sendButton && !sendButton.disabled) {
|
|
600
|
-
sendButton.click()
|
|
601
|
-
}
|
|
602
|
-
} catch (error) {
|
|
603
|
-
console.error('Failed to redo message:', error)
|
|
604
|
-
errorStatus.value = {
|
|
605
|
-
errorCode: 'Error',
|
|
606
|
-
message: 'Failed to redo message: ' + error.message,
|
|
607
|
-
stackTrace: null
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Edit a user message
|
|
613
|
-
const editMessage = (message) => {
|
|
614
|
-
if (!currentThread.value || message.role !== 'user') return
|
|
615
|
-
|
|
616
|
-
editingMessage.value = message
|
|
617
|
-
editingMessageId.value = message.id
|
|
618
|
-
// Extract the actual message content (remove media indicators if present)
|
|
619
|
-
let messageContent = message.content
|
|
620
|
-
messageContent = messageContent.replace(/\n\n\[[🖼️🔉📎] [^\]]+\]$/, '')
|
|
621
|
-
editingMessageContent.value = messageContent
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Save edited message
|
|
625
|
-
const saveEditedMessage = async () => {
|
|
626
|
-
if (!currentThread.value || !editingMessage.value || !editingMessageContent.value.trim()) return
|
|
627
|
-
|
|
628
|
-
try {
|
|
629
|
-
const threadId = currentThread.value.id
|
|
630
|
-
const messageId = editingMessage.value.id
|
|
631
|
-
const updatedContent = editingMessageContent.value
|
|
632
|
-
|
|
633
|
-
// Update the message content
|
|
634
|
-
editingMessage.value.content = updatedContent
|
|
635
|
-
await threads.updateMessageInThread(threadId, messageId, { content: updatedContent })
|
|
636
|
-
|
|
637
|
-
// Clear editing state
|
|
638
|
-
editingMessageId.value = null
|
|
639
|
-
editingMessageContent.value = ''
|
|
640
|
-
editingMessage.value = null
|
|
641
|
-
|
|
642
|
-
// Now redo the message (clear all responses after it and re-run)
|
|
643
|
-
await nextTick()
|
|
644
|
-
await threads.redoMessageFromThread(threadId, messageId)
|
|
645
|
-
|
|
646
|
-
// Set the message text in the chat prompt
|
|
647
|
-
chatPrompt.messageText.value = updatedContent
|
|
648
|
-
|
|
649
|
-
// Clear any attached files since we're re-running
|
|
650
|
-
chatPrompt.attachedFiles.value = []
|
|
651
|
-
|
|
652
|
-
// Trigger send by simulating the send action
|
|
653
|
-
await nextTick()
|
|
654
|
-
|
|
655
|
-
// Find the send button and click it
|
|
656
|
-
const sendButton = document.querySelector('button[title*="Send"]')
|
|
657
|
-
if (sendButton && !sendButton.disabled) {
|
|
658
|
-
sendButton.click()
|
|
659
|
-
}
|
|
660
|
-
} catch (error) {
|
|
661
|
-
console.error('Failed to save edited message:', error)
|
|
662
|
-
errorStatus.value = {
|
|
663
|
-
errorCode: 'Error',
|
|
664
|
-
message: 'Failed to save edited message: ' + error.message,
|
|
665
|
-
stackTrace: null
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Cancel editing
|
|
671
|
-
const cancelEdit = () => {
|
|
672
|
-
editingMessageId.value = null
|
|
673
|
-
editingMessageContent.value = ''
|
|
674
|
-
editingMessage.value = null
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function tokensTitle(usage) {
|
|
678
|
-
let title = []
|
|
679
|
-
if (usage.tokens && usage.price) {
|
|
680
|
-
const msg = parseFloat(usage.price) > 0
|
|
681
|
-
? `${usage.tokens} tokens @ ${usage.price} = ${tokenCost(usage.price, usage.tokens)}`
|
|
682
|
-
: `${usage.tokens} tokens`
|
|
683
|
-
const duration = usage.duration ? ` in ${usage.duration}ms` : ''
|
|
684
|
-
title.push(msg + duration)
|
|
685
|
-
}
|
|
686
|
-
return title.join('\n')
|
|
687
|
-
}
|
|
688
|
-
const numFmt = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', minimumFractionDigits: 6 })
|
|
689
|
-
function tokenCost(price, tokens) {
|
|
690
|
-
if (!price || !tokens) return ''
|
|
691
|
-
return numFmt.format(parseFloat(price) * tokens)
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
onMounted(() => {
|
|
695
|
-
setTimeout(addCopyButtons, 1)
|
|
696
|
-
})
|
|
697
|
-
|
|
698
|
-
return {
|
|
699
|
-
config,
|
|
700
|
-
models,
|
|
701
|
-
threads,
|
|
702
|
-
prompts,
|
|
703
|
-
isGenerating,
|
|
704
|
-
customPromptValue,
|
|
705
|
-
currentThread,
|
|
706
|
-
selectedModel,
|
|
707
|
-
selectedPrompt,
|
|
708
|
-
currentSystemPrompt,
|
|
709
|
-
showSystemPrompt,
|
|
710
|
-
messagesContainer,
|
|
711
|
-
errorStatus,
|
|
712
|
-
copying,
|
|
713
|
-
editingMessageId,
|
|
714
|
-
editingMessageContent,
|
|
715
|
-
editingMessage,
|
|
716
|
-
formatTime,
|
|
717
|
-
renderMarkdown,
|
|
718
|
-
isReasoningExpanded,
|
|
719
|
-
toggleReasoning,
|
|
720
|
-
formatReasoning,
|
|
721
|
-
copyMessageContent,
|
|
722
|
-
redoMessage,
|
|
723
|
-
editMessage,
|
|
724
|
-
saveEditedMessage,
|
|
725
|
-
cancelEdit,
|
|
726
|
-
configUpdated,
|
|
727
|
-
exportThreads,
|
|
728
|
-
isExporting,
|
|
729
|
-
triggerImport,
|
|
730
|
-
handleFileImport,
|
|
731
|
-
isImporting,
|
|
732
|
-
fileInput,
|
|
733
|
-
tokensTitle,
|
|
734
|
-
humanifyMs,
|
|
735
|
-
humanifyNumber,
|
|
736
|
-
formatCost,
|
|
737
|
-
statsTitle,
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|