@ouraihub/aether-ui 0.1.0 → 0.1.2
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.
- package/package.json +3 -3
- package/src/chat/ApprovalCard.svelte +8 -2
- package/src/chat/ChatComposer.svelte +319 -0
- package/src/chat/ChatMessage.svelte +262 -0
- package/src/chat/ChatPanel.css +386 -0
- package/src/chat/ChatPanel.svelte +276 -1880
- package/src/chat/ChatPanelApprovals.svelte +28 -0
- package/src/chat/ChatPanelComposer.svelte +336 -0
- package/src/chat/ChatPanelDialogs.svelte +242 -0
- package/src/chat/ChatPanelHeader.svelte +108 -0
- package/src/chat/ChatPanelMessages.svelte +166 -0
- package/src/chat/ChatPanelMode.svelte +190 -0
- package/src/chat/ChatPanelState.svelte.ts +1 -0
- package/src/chat/ChatPanelToolbar.svelte +436 -0
- package/src/chat/ChatSurface.svelte +168 -0
- package/src/chat/ContextUsageStatus.svelte +97 -0
- package/src/chat/MessageItem.svelte +111 -214
- package/src/chat/ModelEffortPicker.svelte +326 -0
- package/src/chat/PickerEffort.svelte +71 -0
- package/src/chat/ReasoningControl.svelte +184 -0
- package/src/chat/ReasoningSlider.svelte +174 -0
- package/src/chat/SkillPicker.svelte +14 -6
- package/src/chat/UsageControl.svelte +223 -0
- package/src/chat/UsageDetails.svelte +44 -90
- package/src/chat/chat-attachments.svelte.ts +78 -0
- package/src/chat/chat-panel-history.svelte.ts +430 -0
- package/src/chat/chat-panel-models.svelte.ts +97 -0
- package/src/chat/chat-panel-props.ts +38 -0
- package/src/chat/context-status.ts +101 -0
- package/src/chat/icons/LICENSE-REMIX +186 -0
- package/src/chat/icons/LICENSE-SIMPLE-ICONS +30 -0
- package/src/chat/icons/PathIcon.svelte +8 -0
- package/src/chat/icons/effort-icons.ts +65 -0
- package/src/chat/icons/status-icons.ts +105 -0
- package/src/chat/index.ts +25 -1
- package/src/chat/model-effort.ts +177 -0
- package/src/chat/run-session-history.ts +56 -0
- package/src/chat/run-session-types.ts +69 -0
- package/src/chat/run-session.svelte.ts +73 -135
- package/src/chat/test-support/chat-fixture.ts +194 -0
- package/src/chat/test-support/conversation-fixtures.ts +32 -0
- package/src/chat/test-support/run-session-fixture.ts +112 -0
- package/src/dev/Demo.svelte +3 -1
- package/src/i18n/en-chat.d.ts +1 -0
- package/src/i18n/en-chat.js +201 -0
- package/src/i18n/en.ts +58 -180
- package/src/i18n/index.ts +7 -2
- package/src/i18n/zh-CN-chat.d.ts +1 -0
- package/src/i18n/zh-CN-chat.js +190 -0
- package/src/i18n/zh-CN.ts +8 -153
- package/src/logs/LogPanel.svelte +9 -3
- package/src/logs/LogSettings.svelte +3 -1
- package/src/mcp/McpClientEditor.svelte +204 -0
- package/src/mcp/McpClientSettings.svelte +57 -130
- package/src/mcp/McpServerSettings.svelte +47 -11
- package/src/mcp/input.ts +2 -1
- package/src/ports/contract.ts +11 -2
- package/src/ports/fake-optional.ts +81 -0
- package/src/ports/fake.ts +90 -86
- package/src/ports/search.ts +11 -2
- package/src/ports/test-support/fake-fixture.ts +63 -0
- package/src/providers/EndpointKeysDialog.svelte +27 -13
- package/src/providers/EndpointModels.svelte +311 -0
- package/src/providers/EndpointPanel.svelte +33 -258
- package/src/providers/ModelPickerDialog.svelte +59 -16
- package/src/providers/ModelPricing.svelte +125 -0
- package/src/providers/ModelSelect.svelte +22 -8
- package/src/providers/ModelSettingsDialog.svelte +61 -126
- package/src/providers/ProviderDialogs.svelte +120 -0
- package/src/providers/ProviderList.svelte +159 -0
- package/src/providers/ProvidersSettings.svelte +67 -207
- package/src/providers/RequestConfigDialog.svelte +40 -12
- package/src/providers/SecretInput.svelte +11 -3
- package/src/providers/draft.ts +39 -13
- package/src/providers/edit-session.ts +31 -7
- package/src/providers/reasoning.ts +18 -4
- package/src/providers/revision.svelte.ts +3 -1
- package/src/providers/test-support/providers-fixture.ts +120 -0
- package/src/search/SearchAdvancedTools.svelte +182 -0
- package/src/search/SearchProviderCard.svelte +195 -0
- package/src/search/SearchSettings.svelte +62 -265
- package/src/search/settings-flow.ts +8 -3
- package/src/skills/SkillAdd.svelte +51 -18
- package/src/skills/SkillAuthoring.svelte +69 -20
- package/src/skills/SkillEditor.svelte +13 -3
- package/src/skills/SkillPreviewDialog.svelte +19 -5
- package/src/skills/SkillsSettings.svelte +11 -4
- package/src/skills/test-support/skills-fixture.ts +139 -0
- package/src/tauri/index.ts +23 -7
- package/src/web/index.ts +90 -25
|
@@ -1,61 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onDestroy,
|
|
3
|
-
import { SvelteMap } from 'svelte/reactivity'
|
|
4
|
-
import XIcon from '@lucide/svelte/icons/x'
|
|
5
|
-
import PlusIcon from '@lucide/svelte/icons/plus'
|
|
6
|
-
import HistoryIcon from '@lucide/svelte/icons/history'
|
|
7
|
-
import GlobeIcon from '@lucide/svelte/icons/globe'
|
|
8
|
-
import MaximizeIcon from '@lucide/svelte/icons/maximize-2'
|
|
9
|
-
import MinimizeIcon from '@lucide/svelte/icons/minimize-2'
|
|
10
|
-
import ArrowDownIcon from '@lucide/svelte/icons/arrow-down'
|
|
11
|
-
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up'
|
|
12
|
-
import SquareIcon from '@lucide/svelte/icons/square'
|
|
13
|
-
import GaugeIcon from '@lucide/svelte/icons/gauge'
|
|
14
|
-
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'
|
|
15
|
-
import PaperclipIcon from '@lucide/svelte/icons/paperclip'
|
|
16
|
-
import MessageSquarePlusIcon from '@lucide/svelte/icons/message-square-plus'
|
|
17
|
-
import ImageIcon from '@lucide/svelte/icons/image'
|
|
18
|
-
import CheckIcon from '@lucide/svelte/icons/check'
|
|
19
|
-
import GripVerticalIcon from '@lucide/svelte/icons/grip-vertical'
|
|
20
|
-
import Settings2Icon from '@lucide/svelte/icons/settings-2'
|
|
21
|
-
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw'
|
|
22
|
-
import ShieldIcon from '@lucide/svelte/icons/shield'
|
|
23
|
-
import HandIcon from '@lucide/svelte/icons/hand'
|
|
24
|
-
import RouteIcon from '@lucide/svelte/icons/route'
|
|
25
|
-
import FilePenIcon from '@lucide/svelte/icons/file-pen'
|
|
26
|
-
import ShieldCheckIcon from '@lucide/svelte/icons/shield-check'
|
|
27
|
-
import ShieldAlertIcon from '@lucide/svelte/icons/shield-alert'
|
|
28
|
-
import InfoIcon from '@lucide/svelte/icons/info'
|
|
29
|
-
import * as DropdownMenu from '#lib/components/ui/dropdown-menu/index.js'
|
|
30
|
-
import { Switch } from '#lib/components/ui/switch/index.js'
|
|
31
|
-
import { Popover } from 'bits-ui'
|
|
32
|
-
import { Button } from '#lib/components/ui/button/index.js'
|
|
33
|
-
import { Input } from '#lib/components/ui/input/index.js'
|
|
34
|
-
import { Textarea } from '#lib/components/ui/textarea/index.js'
|
|
35
|
-
import * as Empty from '#lib/components/ui/empty/index.js'
|
|
36
|
-
import * as Alert from '#lib/components/ui/alert/index.js'
|
|
37
|
-
import * as Dialog from '#lib/components/ui/dialog/index.js'
|
|
38
|
-
import * as Field from '#lib/components/ui/field/index.js'
|
|
2
|
+
import { onDestroy, untrack } from 'svelte'
|
|
39
3
|
import { getKitContext } from '../context'
|
|
40
4
|
import {
|
|
41
5
|
isKitClientError,
|
|
42
|
-
type ConversationSummary,
|
|
43
|
-
type Conversation,
|
|
44
6
|
type ModelRef,
|
|
45
7
|
type SkillRef,
|
|
46
|
-
type ModelOption,
|
|
47
|
-
type ImagePart,
|
|
48
|
-
type ImageRef,
|
|
49
8
|
type ReasoningEffort,
|
|
50
9
|
type PermissionMode,
|
|
51
10
|
} from '../ports'
|
|
52
|
-
import
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
55
|
-
import
|
|
56
|
-
import
|
|
11
|
+
import { providersRevision } from '../providers/revision.svelte'
|
|
12
|
+
import { RunSession } from './run-session.svelte'
|
|
13
|
+
import { ChatPanelHistoryState } from './chat-panel-history.svelte'
|
|
14
|
+
import { ChatAttachments, hasImages, type AttachedImage } from './chat-attachments.svelte'
|
|
15
|
+
import ChatSurface from './ChatSurface.svelte'
|
|
16
|
+
import ChatPanelApprovals from './ChatPanelApprovals.svelte'
|
|
57
17
|
import { nearestEffort, REASONING_EFFORTS } from '../providers/reasoning'
|
|
58
|
-
import
|
|
18
|
+
import ChatPanelHeader from './ChatPanelHeader.svelte'
|
|
19
|
+
import ChatPanelMessages from './ChatPanelMessages.svelte'
|
|
20
|
+
import ChatPanelComposer from './ChatPanelComposer.svelte'
|
|
21
|
+
import ChatPanelDialogs from './ChatPanelDialogs.svelte'
|
|
22
|
+
import type { ChatPanelProps } from './chat-panel-props'
|
|
23
|
+
import { ChatPanelModels } from './chat-panel-models.svelte'
|
|
24
|
+
import { modelKey, type PickerValue } from './model-effort'
|
|
59
25
|
|
|
60
26
|
let {
|
|
61
27
|
purpose = 'chat',
|
|
@@ -77,58 +43,36 @@
|
|
|
77
43
|
onToolbarChange,
|
|
78
44
|
reasoningEffort: reasoningEffortProp,
|
|
79
45
|
onReasoningEffortChange,
|
|
46
|
+
defaultModel = null,
|
|
47
|
+
defaultReasoningEffort = 'default',
|
|
48
|
+
onDefaultModelChange,
|
|
80
49
|
permissionModes = ['default', 'plan', 'accept_edits'],
|
|
81
50
|
permissionMode = $bindable('default'),
|
|
82
|
-
}:
|
|
83
|
-
purpose?: string
|
|
84
|
-
model?: ModelRef | null
|
|
85
|
-
scope?: string
|
|
86
|
-
/** Host context can be removed for the next message without changing conversation ownership. */
|
|
87
|
-
context?: { kind: string; id: string; label: string } | null
|
|
88
|
-
skills?: SkillRef[]
|
|
89
|
-
onSkillsChange?: (refs: SkillRef[]) => void
|
|
90
|
-
skillsBudget?: number
|
|
91
|
-
onInsert?: (markdown: string) => void
|
|
92
|
-
onSaveNote?: (markdown: string, images: ImageRef[]) => Promise<void> | void
|
|
93
|
-
onExportMarkdown?: (markdown: string) => Promise<void> | void
|
|
94
|
-
onOpenSettings?: () => void
|
|
95
|
-
/** A failed restore stays visible until retried or explicitly replaced with a new conversation. */
|
|
96
|
-
conversationId?: string | null
|
|
97
|
-
onConversationChange?: (id: string | null) => void
|
|
98
|
-
onClose?: () => void
|
|
99
|
-
/** Whether the host has a painting model bound; gates “Generate image” (docs/20 §1). */
|
|
100
|
-
paintingModel?: boolean
|
|
101
|
-
/** Ordered ids of composer tools pinned to the toolbar (docs/38); omitted means the default. */
|
|
102
|
-
toolbar?: string[]
|
|
103
|
-
onToolbarChange?: (ids: string[]) => void
|
|
104
|
-
/** Saved effort choice (host preference); omitted means `default`. Applied per model as its nearest allowed effort. */
|
|
105
|
-
reasoningEffort?: ReasoningEffort
|
|
106
|
-
onReasoningEffortChange?: (effort: ReasoningEffort) => void
|
|
107
|
-
/** Modes the host allows for this panel's runs (same list as `KitHost::permission_modes`). */
|
|
108
|
-
permissionModes?: PermissionMode[]
|
|
109
|
-
/** Bindable non-dangerous preference; full access stays private and is consumed once. */
|
|
110
|
-
permissionMode?: PermissionMode
|
|
111
|
-
} = $props()
|
|
51
|
+
}: ChatPanelProps = $props()
|
|
112
52
|
|
|
113
53
|
const { ports, i18n, notify } = getKitContext()
|
|
114
|
-
let viewGeneration = 0
|
|
115
|
-
let listGeneration = 0
|
|
116
|
-
let disposed = false
|
|
117
54
|
const { t } = i18n
|
|
118
55
|
const history = ports.conversations
|
|
119
56
|
const session = ports.chat ? new RunSession(ports.chat, history) : null
|
|
120
57
|
// svelte-ignore state_referenced_locally
|
|
121
58
|
let picked = $state<ModelRef | null>(model)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
59
|
+
const errorText = (error: unknown) =>
|
|
60
|
+
t(`errors.${isKitClientError(error) ? error.code : 'internal'}`)
|
|
61
|
+
const models = new ChatPanelModels(ports.providers, i18n, errorText)
|
|
62
|
+
const modelOptions = $derived(models.options)
|
|
63
|
+
const modelError = $derived(models.error)
|
|
125
64
|
const capability = $derived(
|
|
126
|
-
modelOptions?.find(
|
|
65
|
+
modelOptions?.find(
|
|
66
|
+
(item) => item.endpointId === picked?.endpointId && item.modelId === picked?.modelId,
|
|
67
|
+
) ?? null,
|
|
127
68
|
)
|
|
128
|
-
// The saved choice survives model switches; each model sends its nearest allowed effort
|
|
69
|
+
// The saved choice survives model switches; each model sends its nearest allowed effort
|
|
70
|
+
// (docs/28 RT3).
|
|
129
71
|
// svelte-ignore state_referenced_locally
|
|
130
72
|
let chosenEffort = $state<ReasoningEffort>(reasoningEffortProp ?? 'default')
|
|
131
|
-
const allowedEfforts = $derived(
|
|
73
|
+
const allowedEfforts = $derived(
|
|
74
|
+
REASONING_EFFORTS.filter((effort) => capability?.reasoning?.efforts.includes(effort)),
|
|
75
|
+
)
|
|
132
76
|
const reasoningEffort = $derived(nearestEffort(chosenEffort, allowedEfforts))
|
|
133
77
|
function chooseEffort(effort: ReasoningEffort) {
|
|
134
78
|
if (session?.running || effort === chosenEffort) return
|
|
@@ -136,7 +80,10 @@
|
|
|
136
80
|
onReasoningEffortChange?.(effort)
|
|
137
81
|
}
|
|
138
82
|
const MODES: PermissionMode[] = ['default', 'plan', 'accept_edits', 'auto', 'full_access']
|
|
139
|
-
const
|
|
83
|
+
const isAgent = $derived(purpose === 'agent')
|
|
84
|
+
const allowedModes: PermissionMode[] = $derived(
|
|
85
|
+
isAgent ? MODES.filter((mode) => permissionModes.includes(mode)) : ['default'],
|
|
86
|
+
)
|
|
140
87
|
let fullAccess = $state(false)
|
|
141
88
|
const mode: PermissionMode = $derived(
|
|
142
89
|
fullAccess && allowedModes.includes('full_access')
|
|
@@ -147,7 +94,7 @@
|
|
|
147
94
|
)
|
|
148
95
|
async function chooseMode(next: PermissionMode) {
|
|
149
96
|
if (session?.running || next === mode || !allowedModes.includes(next)) return
|
|
150
|
-
const generation = viewGeneration
|
|
97
|
+
const generation = historyState.viewGeneration
|
|
151
98
|
if (next === 'full_access') {
|
|
152
99
|
if (
|
|
153
100
|
!(await notify.confirm({
|
|
@@ -157,7 +104,12 @@
|
|
|
157
104
|
}))
|
|
158
105
|
)
|
|
159
106
|
return
|
|
160
|
-
if (
|
|
107
|
+
if (
|
|
108
|
+
!historyState.disposed &&
|
|
109
|
+
generation === historyState.viewGeneration &&
|
|
110
|
+
!session?.running &&
|
|
111
|
+
allowedModes.includes(next)
|
|
112
|
+
)
|
|
161
113
|
fullAccess = true
|
|
162
114
|
} else {
|
|
163
115
|
fullAccess = false
|
|
@@ -165,53 +117,38 @@
|
|
|
165
117
|
}
|
|
166
118
|
}
|
|
167
119
|
let reasoningOpen = $state(false)
|
|
168
|
-
const reasoningId = $props.id()
|
|
169
|
-
let reasoningTrigger = $state<HTMLButtonElement | null>(null)
|
|
170
|
-
const effortLabel = $derived(t(`chat.effort.${reasoningEffort}`))
|
|
171
|
-
const effortIndex = $derived(
|
|
172
|
-
reasoningEffort === 'default' ? Math.floor(allowedEfforts.length / 2) : allowedEfforts.indexOf(reasoningEffort),
|
|
173
|
-
)
|
|
174
120
|
const noModels = $derived(modelOptions !== null && modelOptions.length === 0)
|
|
175
|
-
const errorText = (error: unknown) => t(`errors.${isKitClientError(error) ? error.code : 'internal'}`)
|
|
176
121
|
$effect(() => {
|
|
177
|
-
|
|
122
|
+
// A provider save swaps options in place: nulling them would reset the chosen effort.
|
|
178
123
|
void providersRevision.value
|
|
179
|
-
|
|
180
|
-
// A provider save swaps options in place: nulling them would reset the chosen effort (docs/41 §3.5).
|
|
181
|
-
modelError = null
|
|
182
|
-
if (!ports.providers) {
|
|
183
|
-
modelOptions = []
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
void ports.providers.modelOptions({ modelType: 'chat' }).then(
|
|
187
|
-
(list) => {
|
|
188
|
-
if (alive) modelOptions = list
|
|
189
|
-
},
|
|
190
|
-
(error: unknown) => {
|
|
191
|
-
if (alive) modelError = errorText(error)
|
|
192
|
-
},
|
|
193
|
-
)
|
|
194
|
-
return () => {
|
|
195
|
-
alive = false
|
|
196
|
-
}
|
|
124
|
+
untrack(() => void models.load())
|
|
197
125
|
})
|
|
198
126
|
$effect(() => {
|
|
199
127
|
// A model that turns out not to take tools cannot keep a pending generate_image request.
|
|
200
128
|
if (capability?.supportsTools === false) generateImage = false
|
|
201
129
|
})
|
|
202
|
-
function commitReasoning(event: Event) {
|
|
203
|
-
const effort = allowedEfforts[Number((event.currentTarget as HTMLInputElement).value)]
|
|
204
|
-
if (effort) chooseEffort(effort)
|
|
205
|
-
}
|
|
206
130
|
function pickModel(next: ModelRef) {
|
|
207
131
|
picked = next
|
|
208
132
|
session?.resetContext()
|
|
209
133
|
}
|
|
134
|
+
const pickerValue = $derived(
|
|
135
|
+
picked ? { key: modelKey(picked.endpointId, picked.modelId), effort: reasoningEffort } : null,
|
|
136
|
+
)
|
|
137
|
+
const pickerDefault = $derived(
|
|
138
|
+
defaultModel
|
|
139
|
+
? {
|
|
140
|
+
key: modelKey(defaultModel.endpointId, defaultModel.modelId),
|
|
141
|
+
effort: defaultReasoningEffort,
|
|
142
|
+
}
|
|
143
|
+
: null,
|
|
144
|
+
)
|
|
145
|
+
function pickValue(value: PickerValue) {
|
|
146
|
+
if (!session?.running) models.select(value, picked, chosenEffort, pickModel, chooseEffort)
|
|
147
|
+
}
|
|
210
148
|
|
|
211
149
|
let draft = $state('')
|
|
212
150
|
let draftRevision = 0
|
|
213
151
|
let contextRevision = 0
|
|
214
|
-
let expanded = $state(false)
|
|
215
152
|
// svelte-ignore state_referenced_locally
|
|
216
153
|
let ownSkills = $state<SkillRef[]>(skills ?? [])
|
|
217
154
|
const selected = $derived(skills ?? ownSkills)
|
|
@@ -229,458 +166,114 @@
|
|
|
229
166
|
scope: scope ?? null,
|
|
230
167
|
language: i18n.locale,
|
|
231
168
|
model: picked,
|
|
232
|
-
skills: selected,
|
|
169
|
+
skills: isAgent ? selected : [],
|
|
233
170
|
reasoningEffort,
|
|
234
171
|
permissionMode: mode,
|
|
235
172
|
generateImage: purpose === 'chat' && generateImage && paintingModel,
|
|
236
173
|
context: activeContext ? { kind: activeContext.kind, id: activeContext.id } : null,
|
|
237
174
|
})
|
|
238
|
-
// Consume before awaiting run acceptance, including retries and translations.
|
|
175
|
+
// Consume before awaiting run acceptance, including retries and translations.
|
|
176
|
+
// Never persist full access.
|
|
239
177
|
function consumeBase() {
|
|
240
178
|
const request = { ...base }
|
|
241
179
|
fullAccess = false
|
|
242
180
|
return request
|
|
243
181
|
}
|
|
244
|
-
const currentContext = $derived(
|
|
245
|
-
session?.contextUsage &&
|
|
246
|
-
session.contextUsage.endpointId === picked?.endpointId &&
|
|
247
|
-
session.contextUsage.modelId === picked?.modelId
|
|
248
|
-
? session.contextUsage
|
|
249
|
-
: null,
|
|
250
|
-
)
|
|
251
182
|
|
|
252
|
-
let
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
183
|
+
let scrollRevision = $state(0)
|
|
184
|
+
const historyState = new ChatPanelHistoryState(
|
|
185
|
+
{
|
|
186
|
+
history,
|
|
187
|
+
getSession: () => session,
|
|
188
|
+
getModelOptions: () => modelOptions,
|
|
189
|
+
getModelError: () => modelError,
|
|
190
|
+
setDraft: (value) => {
|
|
191
|
+
draft = value
|
|
192
|
+
},
|
|
193
|
+
bumpDraftRevision: () => draftRevision++,
|
|
194
|
+
setFullAccess: (value) => {
|
|
195
|
+
fullAccess = value
|
|
196
|
+
},
|
|
197
|
+
getScope: () => scope,
|
|
198
|
+
getLocale: () => i18n.locale,
|
|
199
|
+
onConversationChange: (id) => onConversationChange?.(id),
|
|
200
|
+
bumpScrollRevision: () => scrollRevision++,
|
|
201
|
+
notify,
|
|
202
|
+
t,
|
|
203
|
+
errorText,
|
|
204
|
+
consumeBase,
|
|
205
|
+
discardRecovery: () => discardRecovery(),
|
|
206
|
+
},
|
|
207
|
+
untrack(() => conversationId),
|
|
258
208
|
)
|
|
259
|
-
|
|
260
|
-
let historyBusy = $state(false)
|
|
261
|
-
let historyLoading = $state(false)
|
|
262
|
-
let historyError = $state<string | null>(null)
|
|
263
|
-
let retryHistory: () => Promise<void> = refresh
|
|
264
|
-
let renameTitle = $state('')
|
|
265
|
-
let renaming = $state(false)
|
|
266
|
-
// svelte-ignore state_referenced_locally
|
|
267
|
-
let reported = conversationId
|
|
268
|
-
let restored = $state(false)
|
|
269
|
-
let restoreBlocked = $state(false)
|
|
270
|
-
$effect(() => () => {
|
|
271
|
-
disposed = true
|
|
272
|
-
viewGeneration++
|
|
273
|
-
listGeneration++
|
|
274
|
-
fullAccess = false
|
|
275
|
-
session?.dispose()
|
|
276
|
-
})
|
|
277
|
-
$effect(() => {
|
|
278
|
-
if (session && !session.running) void refresh()
|
|
279
|
-
})
|
|
209
|
+
$effect(() => () => historyState.dispose())
|
|
280
210
|
$effect(() => {
|
|
281
|
-
|
|
282
|
-
if (!restored || restoreBlocked || id === reported) return
|
|
283
|
-
reported = id
|
|
284
|
-
onConversationChange?.(id)
|
|
211
|
+
if (session && !session.running) void historyState.refresh()
|
|
285
212
|
})
|
|
286
|
-
|
|
287
|
-
void restore(conversationId)
|
|
213
|
+
$effect(() => historyState.syncConversation())
|
|
214
|
+
void historyState.restore(untrack(() => conversationId))
|
|
288
215
|
|
|
289
|
-
|
|
290
|
-
if (!history) return
|
|
291
|
-
const generation = ++listGeneration
|
|
292
|
-
historyLoading = true
|
|
293
|
-
try {
|
|
294
|
-
const next = await history.list(scope)
|
|
295
|
-
if (disposed || generation !== listGeneration) return
|
|
296
|
-
conversations = next
|
|
297
|
-
if (retryHistory === refresh) historyError = null
|
|
298
|
-
} catch (error) {
|
|
299
|
-
if (!disposed && generation === listGeneration && !restoreBlocked) {
|
|
300
|
-
historyError = errorText(error)
|
|
301
|
-
retryHistory = refresh
|
|
302
|
-
}
|
|
303
|
-
} finally {
|
|
304
|
-
if (!disposed && generation === listGeneration) historyLoading = false
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
async function restore(id: string | null) {
|
|
308
|
-
if (id !== null && history && session) {
|
|
309
|
-
restoreBlocked = true
|
|
310
|
-
await open(id)
|
|
311
|
-
}
|
|
312
|
-
restored = true
|
|
313
|
-
}
|
|
314
|
-
async function open(id: string) {
|
|
315
|
-
if (!history || !session || session.running || historyBusy) return
|
|
316
|
-
const generation = ++viewGeneration
|
|
317
|
-
fullAccess = false
|
|
318
|
-
historyBusy = true
|
|
319
|
-
try {
|
|
320
|
-
const conversation = await history.get(id)
|
|
321
|
-
if (disposed || generation !== viewGeneration) return
|
|
322
|
-
session.show(id, conversation.turns, conversation.summary.selectedTurnId)
|
|
323
|
-
shownSummary = conversation.summary
|
|
324
|
-
restoreBlocked = false
|
|
325
|
-
restored = true
|
|
326
|
-
historyError = null
|
|
327
|
-
retryHistory = refresh
|
|
328
|
-
historyOpen = false
|
|
329
|
-
renaming = false
|
|
330
|
-
follow = true
|
|
331
|
-
} catch (error) {
|
|
332
|
-
if (disposed || generation !== viewGeneration) return
|
|
333
|
-
historyError = errorText(error)
|
|
334
|
-
retryHistory = () => open(id)
|
|
335
|
-
} finally {
|
|
336
|
-
if (!disposed && generation === viewGeneration) historyBusy = false
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
function newConversation() {
|
|
340
|
-
if (!session || session.running || historyBusy) return
|
|
341
|
-
viewGeneration++
|
|
342
|
-
fullAccess = false
|
|
343
|
-
session.show(null)
|
|
344
|
-
shownSummary = null
|
|
345
|
-
restoreBlocked = false
|
|
346
|
-
restored = true
|
|
347
|
-
historyError = null
|
|
348
|
-
historyOpen = false
|
|
349
|
-
renaming = false
|
|
350
|
-
follow = true
|
|
351
|
-
}
|
|
352
|
-
async function rename() {
|
|
353
|
-
const id = session?.conversationId
|
|
354
|
-
const next = renameTitle.trim()
|
|
355
|
-
if (!history || !id || !next || session?.running || historyBusy) return
|
|
356
|
-
const generation = viewGeneration
|
|
357
|
-
historyBusy = true
|
|
358
|
-
try {
|
|
359
|
-
const summary = await history.rename(id, next)
|
|
360
|
-
if (disposed || generation !== viewGeneration) return
|
|
361
|
-
shownSummary = summary
|
|
362
|
-
conversations = conversations.map((c) => (c.id === id ? summary : c))
|
|
363
|
-
renaming = false
|
|
364
|
-
historyError = null
|
|
365
|
-
} catch (error) {
|
|
366
|
-
if (disposed || generation !== viewGeneration) return
|
|
367
|
-
historyError = errorText(error)
|
|
368
|
-
retryHistory = rename
|
|
369
|
-
} finally {
|
|
370
|
-
if (!disposed && generation === viewGeneration) historyBusy = false
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
async function remove() {
|
|
374
|
-
const id = session?.conversationId
|
|
375
|
-
if (!history || !session || !id || session.running || historyBusy) return
|
|
376
|
-
const generation = viewGeneration
|
|
377
|
-
historyBusy = true
|
|
378
|
-
try {
|
|
379
|
-
const ok = await notify.confirm({ title: t('chat.deleteTitle'), body: t('chat.deleteBody'), danger: true })
|
|
380
|
-
if (!ok || disposed || generation !== viewGeneration) return
|
|
381
|
-
await history.remove(id)
|
|
382
|
-
if (disposed || generation !== viewGeneration) return
|
|
383
|
-
if (session.conversationId === id) {
|
|
384
|
-
session.show(null)
|
|
385
|
-
shownSummary = null
|
|
386
|
-
historyOpen = false
|
|
387
|
-
renaming = false
|
|
388
|
-
follow = true
|
|
389
|
-
}
|
|
390
|
-
historyError = null
|
|
391
|
-
void refresh()
|
|
392
|
-
} catch (error) {
|
|
393
|
-
if (disposed || generation !== viewGeneration) return
|
|
394
|
-
historyError = errorText(error)
|
|
395
|
-
retryHistory = remove
|
|
396
|
-
} finally {
|
|
397
|
-
if (!disposed && generation === viewGeneration) historyBusy = false
|
|
398
|
-
}
|
|
399
|
-
}
|
|
216
|
+
let followKey = $state(0)
|
|
400
217
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
else grouped.set(turn.questionId, [turn])
|
|
407
|
-
}
|
|
408
|
-
return [...grouped.entries()]
|
|
409
|
-
})
|
|
410
|
-
const actionBusy = $derived(historyBusy || session?.running === true || restoreBlocked)
|
|
411
|
-
let replyDialog = $state<'edit' | 'translate' | null>(null)
|
|
412
|
-
let replyOpen = $state(false)
|
|
413
|
-
let sourceReply = $state<ChatTurn | null>(null)
|
|
414
|
-
let editDraft = $state('')
|
|
415
|
-
let expectedText = ''
|
|
416
|
-
let targetLanguage = $state('')
|
|
417
|
-
let replyModel = $state<ModelRef | null>(null)
|
|
418
|
-
let replyError = $state<string | null>(null)
|
|
419
|
-
const validReplyModel = $derived(
|
|
420
|
-
modelOptions?.some(
|
|
421
|
-
(option) => option.endpointId === replyModel?.endpointId && option.modelId === replyModel?.modelId,
|
|
422
|
-
) === true,
|
|
218
|
+
let contentRevision = 0
|
|
219
|
+
const pendingApprovals = $derived(
|
|
220
|
+
session?.running
|
|
221
|
+
? session.turns.flatMap((turn) => turn.tools.filter((tool) => tool.pending !== null))
|
|
222
|
+
: [],
|
|
423
223
|
)
|
|
424
|
-
function sourceModel(turn: ChatTurn): ModelRef | null {
|
|
425
|
-
return turn.endpointId && turn.modelId ? { endpointId: turn.endpointId, modelId: turn.modelId } : null
|
|
426
|
-
}
|
|
427
|
-
function sourceLabel(turn: ChatTurn) {
|
|
428
|
-
const option = modelOptions?.find((item) => item.endpointId === turn.endpointId && item.modelId === turn.modelId)
|
|
429
|
-
return option
|
|
430
|
-
? (option.name ?? option.modelId)
|
|
431
|
-
: turn.modelId
|
|
432
|
-
? `${turn.endpointId ?? t('chat.unknown')} · ${turn.modelId}`
|
|
433
|
-
: t('chat.noSourceModel')
|
|
434
|
-
}
|
|
435
|
-
function hasChildren(turn: ChatTurn) {
|
|
436
|
-
return session?.turns.some((child) => child.parentId === turn.id || child.supersedesId === turn.id) === true
|
|
437
|
-
}
|
|
438
|
-
function canEdit(turn: ChatTurn) {
|
|
439
|
-
return turn.status !== 'running' && !hasChildren(turn)
|
|
440
|
-
}
|
|
441
|
-
function editReply(turn: ChatTurn) {
|
|
442
|
-
sourceReply = turn
|
|
443
|
-
editDraft = turn.text
|
|
444
|
-
expectedText = turn.text
|
|
445
|
-
replyError = null
|
|
446
|
-
replyDialog = 'edit'
|
|
447
|
-
replyOpen = true
|
|
448
|
-
}
|
|
449
|
-
function translateDialog(turn: ChatTurn) {
|
|
450
|
-
sourceReply = turn
|
|
451
|
-
replyModel = sourceModel(turn)
|
|
452
|
-
targetLanguage = i18n.locale
|
|
453
|
-
replyError = null
|
|
454
|
-
replyDialog = 'translate'
|
|
455
|
-
replyOpen = true
|
|
456
|
-
}
|
|
457
|
-
function showConversation(conversation: Conversation) {
|
|
458
|
-
viewGeneration++
|
|
459
|
-
fullAccess = false
|
|
460
|
-
session?.show(conversation.summary.id, conversation.turns, conversation.summary.selectedTurnId)
|
|
461
|
-
shownSummary = conversation.summary
|
|
462
|
-
follow = true
|
|
463
|
-
}
|
|
464
|
-
async function mutateReply(turn: ChatTurn, action: 'select' | 'remove' | 'branch' | 'copy') {
|
|
465
|
-
const id = session?.conversationId
|
|
466
|
-
if (!history || !session || !id || actionBusy) return
|
|
467
|
-
let generation = viewGeneration
|
|
468
|
-
historyBusy = true
|
|
469
|
-
try {
|
|
470
|
-
if (action === 'remove') {
|
|
471
|
-
if (turn.status === 'running') return
|
|
472
|
-
const reparent = session.turns.some((child) => child.parentId === turn.id)
|
|
473
|
-
if (
|
|
474
|
-
!(await notify.confirm({
|
|
475
|
-
title: t('chat.deleteReplyTitle'),
|
|
476
|
-
body: reparent ? t('chat.deleteReparentBody') : '',
|
|
477
|
-
danger: true,
|
|
478
|
-
}))
|
|
479
|
-
)
|
|
480
|
-
return
|
|
481
|
-
}
|
|
482
|
-
if (disposed || generation !== viewGeneration) return
|
|
483
|
-
const conversation =
|
|
484
|
-
action === 'select' || action === 'branch'
|
|
485
|
-
? await history.selectTurn(id, turn.id)
|
|
486
|
-
: action === 'remove'
|
|
487
|
-
? await history.removeTurn(id, turn.id)
|
|
488
|
-
: await history.fork(id, turn.id)
|
|
489
|
-
if (disposed || generation !== viewGeneration) return
|
|
490
|
-
showConversation(conversation)
|
|
491
|
-
generation = viewGeneration
|
|
492
|
-
if (action === 'branch') {
|
|
493
|
-
draft = ''
|
|
494
|
-
draftRevision++
|
|
495
|
-
discardRecovery()
|
|
496
|
-
}
|
|
497
|
-
historyError = null
|
|
498
|
-
void refresh()
|
|
499
|
-
} catch (error) {
|
|
500
|
-
if (disposed || generation !== viewGeneration) return
|
|
501
|
-
notify.toast('error', errorText(error))
|
|
502
|
-
} finally {
|
|
503
|
-
if (!disposed && generation === viewGeneration) historyBusy = false
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
async function retryReply(turn: ChatTurn) {
|
|
507
|
-
if (!session || actionBusy) return
|
|
508
|
-
const source = sourceModel(turn)
|
|
509
|
-
if (history && turn.persisted) {
|
|
510
|
-
if (modelOptions === null || modelError) {
|
|
511
|
-
notify.toast('error', modelError ?? t('chat.modelsLoading'))
|
|
512
|
-
return
|
|
513
|
-
}
|
|
514
|
-
if (
|
|
515
|
-
!source ||
|
|
516
|
-
!modelOptions.some((option) => option.endpointId === source.endpointId && option.modelId === source.modelId)
|
|
517
|
-
) {
|
|
518
|
-
notify.toast('error', t('chat.sourceModelUnavailable'))
|
|
519
|
-
return
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
follow = true
|
|
523
|
-
await session.retry(consumeBase(), turn.id)
|
|
524
|
-
}
|
|
525
|
-
async function switchModel(turn: ChatTurn, model: ModelRef) {
|
|
526
|
-
if (!session || actionBusy) return
|
|
527
|
-
follow = true
|
|
528
|
-
if (!(await session.retry(consumeBase(), turn.id, model)))
|
|
529
|
-
notify.toast('error', t(`errors.${session.actionError ?? 'internal'}`))
|
|
530
|
-
}
|
|
531
|
-
async function submitReply() {
|
|
532
|
-
const turn = sourceReply
|
|
533
|
-
if (disposed) return
|
|
534
|
-
if (!session || !turn || actionBusy) return
|
|
535
|
-
let generation = viewGeneration
|
|
536
|
-
replyError = null
|
|
537
|
-
if (replyDialog === 'edit') {
|
|
538
|
-
const id = session.conversationId
|
|
539
|
-
if (!history || !id || !editDraft.trim() || editDraft.length > 24_000 || !canEdit(turn)) return
|
|
540
|
-
historyBusy = true
|
|
541
|
-
try {
|
|
542
|
-
const conversation = await history.editTurn(id, turn.id, editDraft, expectedText)
|
|
543
|
-
if (disposed || generation !== viewGeneration) return
|
|
544
|
-
showConversation(conversation)
|
|
545
|
-
generation = viewGeneration
|
|
546
|
-
replyOpen = false
|
|
547
|
-
} catch (error) {
|
|
548
|
-
if (disposed || generation !== viewGeneration) return
|
|
549
|
-
replyError = errorText(error)
|
|
550
|
-
} finally {
|
|
551
|
-
if (!disposed && generation === viewGeneration) historyBusy = false
|
|
552
|
-
}
|
|
553
|
-
} else {
|
|
554
|
-
if (!validReplyModel || !replyModel) return
|
|
555
|
-
if (!targetLanguage.trim() || targetLanguage.trim().length > 80) return
|
|
556
|
-
const accepted = await session.translate({ ...consumeBase(), model: replyModel }, turn.id, targetLanguage.trim())
|
|
557
|
-
if (disposed || generation !== viewGeneration) return
|
|
558
|
-
if (accepted) replyOpen = false
|
|
559
|
-
else replyError = session.actionError ? t(`errors.${session.actionError}`) : t('errors.internal')
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
let scroller = $state<HTMLDivElement | null>(null)
|
|
564
|
-
let follow = $state(true)
|
|
565
|
-
let lastScrollTop = 0
|
|
566
|
-
function onscroll() {
|
|
567
|
-
if (!scroller) return
|
|
568
|
-
const nearBottom = scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop <= 48
|
|
569
|
-
follow = nearBottom && scroller.scrollTop >= lastScrollTop
|
|
570
|
-
lastScrollTop = scroller.scrollTop
|
|
571
|
-
}
|
|
572
|
-
function toBottom() {
|
|
573
|
-
follow = true
|
|
574
|
-
if (scroller) {
|
|
575
|
-
scroller.scrollTop = scroller.scrollHeight
|
|
576
|
-
lastScrollTop = scroller.scrollTop
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
224
|
$effect.pre(() => {
|
|
580
|
-
|
|
581
|
-
for (const turn of session.turns) {
|
|
225
|
+
for (const turn of session?.turns ?? []) {
|
|
582
226
|
void turn.text
|
|
583
227
|
void turn.reasoningText
|
|
584
228
|
void turn.status
|
|
585
229
|
void turn.error
|
|
586
230
|
void turn.usage
|
|
587
231
|
void turn.search
|
|
232
|
+
void turn.generated.length
|
|
588
233
|
for (const tool of turn.tools) {
|
|
589
234
|
void tool.pending
|
|
590
235
|
void tool.summary
|
|
591
236
|
void tool.outcome
|
|
592
237
|
}
|
|
593
238
|
}
|
|
594
|
-
|
|
595
|
-
if (untrack(() => follow))
|
|
596
|
-
void tick().then(() => {
|
|
597
|
-
if (follow) toBottom()
|
|
598
|
-
})
|
|
239
|
+
followKey = ++contentRevision
|
|
599
240
|
})
|
|
600
241
|
function onkeydown(e: KeyboardEvent) {
|
|
601
242
|
if (e.key !== 'Escape' || e.defaultPrevented || e.isComposing) return
|
|
602
243
|
// Popover content renders inline, so Escape inside it bubbles here; let the layer close itself.
|
|
603
244
|
if (
|
|
604
245
|
reasoningOpen ||
|
|
605
|
-
historyOpen ||
|
|
606
|
-
document.querySelector(
|
|
246
|
+
historyState.historyOpen ||
|
|
247
|
+
document.querySelector(
|
|
248
|
+
'[data-popover-content], [data-slot="select-content"], [role="dialog"]',
|
|
249
|
+
)
|
|
607
250
|
)
|
|
608
251
|
return
|
|
609
252
|
if (onClose && !session?.running) onClose()
|
|
610
253
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
// svelte-ignore state_referenced_locally
|
|
622
|
-
let pinned = $state<string[]>(toolbarProp ?? DEFAULT_TOOLBAR)
|
|
623
|
-
let customizing = $state(false)
|
|
624
|
-
/** Tools usable here; unavailable pinned ids stay stored but hidden (docs/38 §3.2). */
|
|
625
|
-
const tools = $derived(
|
|
626
|
-
['new-conversation', 'web-search', 'attachment', 'generate-image'].filter(
|
|
627
|
-
(id) =>
|
|
628
|
-
(id !== 'web-search' || (ports.search && purpose === 'chat')) &&
|
|
629
|
-
(id !== 'generate-image' || purpose === 'chat'),
|
|
630
|
-
),
|
|
254
|
+
const attachments = new ChatAttachments(() => capability?.canVision === true, notify, i18n)
|
|
255
|
+
type Attached = AttachedImage
|
|
256
|
+
const currentContext = $derived(
|
|
257
|
+
draft.trim() === '' &&
|
|
258
|
+
attachments.images.length === 0 &&
|
|
259
|
+
session?.contextUsage &&
|
|
260
|
+
session.contextUsage.endpointId === picked?.endpointId &&
|
|
261
|
+
session.contextUsage.modelId === picked?.modelId
|
|
262
|
+
? session.contextUsage
|
|
263
|
+
: null,
|
|
631
264
|
)
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
...tools.filter((id) => !pinned.includes(id)),
|
|
635
|
-
])
|
|
636
|
-
function setPinned(next: string[]) {
|
|
637
|
-
const all = [...next, ...pinned.filter((id) => !tools.includes(id))]
|
|
638
|
-
if (all.join() === pinned.join()) return
|
|
639
|
-
pinned = all
|
|
640
|
-
onToolbarChange?.(all)
|
|
641
|
-
}
|
|
642
|
-
function moveTool(id: string, to: number) {
|
|
643
|
-
const order = toolOrder.filter((item) => item !== id)
|
|
644
|
-
order.splice(Math.max(0, Math.min(to, order.length)), 0, id)
|
|
645
|
-
setPinned(order.filter((item) => pinned.includes(item)))
|
|
646
|
-
}
|
|
647
|
-
// Window listeners: keyed reordering moves the row node, which would drop pointer capture.
|
|
648
|
-
function dragTool(event: PointerEvent, id: string) {
|
|
649
|
-
const list = (event.currentTarget as HTMLElement).closest('ul')
|
|
650
|
-
if (!list) return
|
|
651
|
-
event.preventDefault()
|
|
652
|
-
const move = (e: PointerEvent) => {
|
|
653
|
-
const over = [...list.children].findIndex((row) => {
|
|
654
|
-
const box = row.getBoundingClientRect()
|
|
655
|
-
return e.clientY >= box.top && e.clientY < box.bottom
|
|
656
|
-
})
|
|
657
|
-
if (over >= 0 && toolOrder[over] !== id) moveTool(id, over)
|
|
658
|
-
}
|
|
659
|
-
const stop = () => {
|
|
660
|
-
window.removeEventListener('pointermove', move)
|
|
661
|
-
window.removeEventListener('pointerup', stop)
|
|
662
|
-
window.removeEventListener('pointercancel', stop)
|
|
663
|
-
}
|
|
664
|
-
window.addEventListener('pointermove', move)
|
|
665
|
-
window.addEventListener('pointerup', stop)
|
|
666
|
-
window.addEventListener('pointercancel', stop)
|
|
667
|
-
}
|
|
668
|
-
const TOOL_LABEL: Record<string, string> = {
|
|
669
|
-
'new-conversation': 'chat.newConversation',
|
|
670
|
-
'web-search': 'chat.network',
|
|
671
|
-
attachment: 'chat.uploadAttachment',
|
|
672
|
-
'generate-image': 'chat.generateImage',
|
|
673
|
-
}
|
|
265
|
+
const reading = $derived(attachments.reading)
|
|
266
|
+
const vision = $derived(capability?.canVision === true)
|
|
674
267
|
const canSend = $derived(
|
|
675
268
|
session !== null &&
|
|
676
269
|
!session.running &&
|
|
677
|
-
restored &&
|
|
678
|
-
!restoreBlocked &&
|
|
679
|
-
!historyBusy &&
|
|
270
|
+
historyState.restored &&
|
|
271
|
+
!historyState.restoreBlocked &&
|
|
272
|
+
!historyState.historyBusy &&
|
|
680
273
|
capability !== null &&
|
|
681
274
|
modelError === null &&
|
|
682
275
|
draft.trim() !== '' &&
|
|
683
|
-
(images.length === 0 || vision) &&
|
|
276
|
+
(attachments.images.length === 0 || vision) &&
|
|
684
277
|
reading === 0,
|
|
685
278
|
)
|
|
686
279
|
let recovery = $state<{
|
|
@@ -694,9 +287,9 @@
|
|
|
694
287
|
recovery = null
|
|
695
288
|
}
|
|
696
289
|
function recoverDraft() {
|
|
697
|
-
if (!recovery || draft !== '' || images.length > 0) return
|
|
290
|
+
if (!recovery || draft !== '' || attachments.images.length > 0) return
|
|
698
291
|
draft = recovery.draft
|
|
699
|
-
images = recovery.images
|
|
292
|
+
attachments.images = recovery.images
|
|
700
293
|
draftRevision++
|
|
701
294
|
if (contextKey === recovery.contextKey) droppedKey = recovery.droppedKey
|
|
702
295
|
contextRevision++
|
|
@@ -704,33 +297,33 @@
|
|
|
704
297
|
}
|
|
705
298
|
async function send() {
|
|
706
299
|
if (!session || !canSend) return
|
|
707
|
-
const generation = viewGeneration
|
|
300
|
+
const generation = historyState.viewGeneration
|
|
708
301
|
const original = draft
|
|
709
|
-
const attached = images
|
|
302
|
+
const attached = attachments.images
|
|
710
303
|
const revision = draftRevision
|
|
711
304
|
const contextVersion = contextRevision
|
|
712
305
|
const removed = droppedKey
|
|
713
306
|
const key = contextKey
|
|
714
307
|
draft = ''
|
|
715
|
-
images = []
|
|
308
|
+
attachments.images = []
|
|
716
309
|
discardRecovery()
|
|
717
|
-
|
|
310
|
+
scrollRevision++
|
|
718
311
|
const accepted = await session.send(
|
|
719
312
|
consumeBase(),
|
|
720
313
|
original.trim(),
|
|
721
314
|
webSearch,
|
|
722
315
|
attached.map(({ mime, data }) => ({ mime, data })),
|
|
723
316
|
)
|
|
724
|
-
if (disposed || generation !== viewGeneration) {
|
|
317
|
+
if (historyState.disposed || generation !== historyState.viewGeneration) {
|
|
725
318
|
for (const image of attached) URL.revokeObjectURL(image.preview)
|
|
726
319
|
return
|
|
727
320
|
}
|
|
728
321
|
if (accepted) {
|
|
729
322
|
for (const image of attached) URL.revokeObjectURL(image.preview)
|
|
730
323
|
if (contextRevision === contextVersion && contextKey === key) droppedKey = null
|
|
731
|
-
} else if (draftRevision === revision && images.length === 0) {
|
|
324
|
+
} else if (draftRevision === revision && attachments.images.length === 0) {
|
|
732
325
|
draft = original
|
|
733
|
-
images = attached
|
|
326
|
+
attachments.images = attached
|
|
734
327
|
if (contextRevision === contextVersion && contextKey === key) droppedKey = removed
|
|
735
328
|
} else {
|
|
736
329
|
recovery = { draft: original, images: attached, droppedKey: removed, contextKey: key }
|
|
@@ -738,1360 +331,163 @@
|
|
|
738
331
|
}
|
|
739
332
|
|
|
740
333
|
onDestroy(() => {
|
|
741
|
-
for (const image of images) URL.revokeObjectURL(image.preview)
|
|
334
|
+
for (const image of attachments.images) URL.revokeObjectURL(image.preview)
|
|
742
335
|
discardRecovery()
|
|
743
336
|
})
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (image) URL.revokeObjectURL(image.preview)
|
|
747
|
-
}
|
|
748
|
-
function readBase64(file: File) {
|
|
749
|
-
const { promise, resolve, reject } = Promise.withResolvers<string>()
|
|
750
|
-
const reader = new FileReader()
|
|
751
|
-
reader.onload = () => resolve(String(reader.result).slice(String(reader.result).indexOf(',') + 1))
|
|
752
|
-
reader.onerror = () => reject(reader.error ?? new Error('read failed'))
|
|
753
|
-
reader.readAsDataURL(file)
|
|
754
|
-
return promise
|
|
755
|
-
}
|
|
756
|
-
/** Adds the image files to the tray, or rejects all of them with a toast. */
|
|
757
|
-
async function attach(files: FileList | null | undefined) {
|
|
758
|
-
const picked = [...(files ?? [])].filter((file) => file.type.startsWith('image/'))
|
|
759
|
-
if (picked.length === 0) return
|
|
760
|
-
if (!vision) notify.toast('error', t('chat.visionUnsupported'))
|
|
761
|
-
else if (picked.some((file) => !IMAGE_MIMES.includes(file.type))) notify.toast('error', t('chat.imageType'))
|
|
762
|
-
else if (picked.some((file) => file.size > MAX_IMAGE_BYTES)) notify.toast('error', t('chat.imageSize'))
|
|
763
|
-
else if (images.length + picked.length > MAX_IMAGES)
|
|
764
|
-
notify.toast('error', t('chat.imageLimit', { max: MAX_IMAGES }))
|
|
765
|
-
else {
|
|
766
|
-
reading++
|
|
767
|
-
try {
|
|
768
|
-
const read = await Promise.all(
|
|
769
|
-
picked.map(async (file) => ({ mime: file.type, data: await readBase64(file), file })),
|
|
770
|
-
)
|
|
771
|
-
// Another add may have landed while reading.
|
|
772
|
-
if (images.length + read.length > MAX_IMAGES) notify.toast('error', t('chat.imageLimit', { max: MAX_IMAGES }))
|
|
773
|
-
else images.push(...read.map(({ mime, data, file }) => ({ mime, data, preview: URL.createObjectURL(file) })))
|
|
774
|
-
} catch {
|
|
775
|
-
notify.toast('error', t('errors.internal'))
|
|
776
|
-
} finally {
|
|
777
|
-
reading--
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
function hasImages(files: FileList | null | undefined) {
|
|
782
|
-
return [...(files ?? [])].some((file) => file.type.startsWith('image/'))
|
|
783
|
-
}
|
|
337
|
+
const removeImage = attachments.remove
|
|
338
|
+
const attach = attachments.attach
|
|
784
339
|
</script>
|
|
785
340
|
|
|
786
341
|
{#if session}
|
|
787
342
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions (Esc bubbles from controls) -->
|
|
788
|
-
<section
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
onclick={() => {
|
|
800
|
-
historyOpen = true
|
|
801
|
-
void refresh()
|
|
802
|
-
}}><HistoryIcon /></Button
|
|
803
|
-
>
|
|
804
|
-
{/if}
|
|
805
|
-
{#if onClose}<Button
|
|
806
|
-
variant="ghost"
|
|
807
|
-
size="icon-sm"
|
|
808
|
-
aria-label={t('chat.close')}
|
|
809
|
-
title={t('chat.close')}
|
|
810
|
-
onclick={onClose}><XIcon /></Button
|
|
811
|
-
>{/if}
|
|
812
|
-
</div>
|
|
813
|
-
<div class="model-row">
|
|
814
|
-
<ModelSelect
|
|
815
|
-
value={picked}
|
|
816
|
-
options={modelOptions}
|
|
817
|
-
filter={{ modelType: 'chat' }}
|
|
818
|
-
disabled={session.running || modelError !== null}
|
|
819
|
-
onchange={pickModel}
|
|
820
|
-
{...onOpenSettings ? { onOpenSettings } : {}}
|
|
821
|
-
/>
|
|
822
|
-
</div>
|
|
823
|
-
</header>
|
|
824
|
-
{#if historyError && !historyOpen}
|
|
825
|
-
<Alert.Root variant="destructive">
|
|
826
|
-
<Alert.Title>{t('chat.historyFailed')}</Alert.Title>
|
|
827
|
-
<Alert.Description
|
|
828
|
-
>{historyError}<Button
|
|
829
|
-
variant="outline"
|
|
830
|
-
size="sm"
|
|
831
|
-
disabled={historyBusy || session.running}
|
|
832
|
-
onclick={() => void retryHistory()}>{t('chat.retryHistory')}</Button
|
|
833
|
-
></Alert.Description
|
|
834
|
-
>
|
|
835
|
-
</Alert.Root>
|
|
836
|
-
{/if}
|
|
837
|
-
{#if session.actionError}<p role="alert" class="text-destructive text-kit-sm">
|
|
838
|
-
{t(`errors.${session.actionError}`)}
|
|
839
|
-
</p>{/if}
|
|
840
|
-
{#if session.historyRefreshError}<p role="alert" class="text-destructive text-kit-sm">
|
|
841
|
-
{t('chat.replyRefreshFailed')}<Button
|
|
842
|
-
variant="outline"
|
|
843
|
-
size="sm"
|
|
844
|
-
disabled={actionBusy}
|
|
845
|
-
onclick={() => session.conversationId && void open(session.conversationId)}>{t('chat.retryHistory')}</Button
|
|
846
|
-
>
|
|
847
|
-
</p>{/if}
|
|
848
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex (native scroll region needs keyboard focus for PageUp/PageDown) -->
|
|
849
|
-
<div class="messages" bind:this={scroller} {onscroll} role="region" aria-label={t('chat.messages')} tabindex="0">
|
|
850
|
-
{#if session.turns.length > 0}
|
|
851
|
-
<ol class="turns">
|
|
852
|
-
{#each groups as [questionId, replies] (questionId)}
|
|
853
|
-
{@const candidates = replies.filter((reply) => reply.kind === 'reply').length > 1}
|
|
854
|
-
{#each replies as turn, i (turn.id)}
|
|
855
|
-
<MessageItem
|
|
856
|
-
{turn}
|
|
857
|
-
{onInsert}
|
|
858
|
-
{onSaveNote}
|
|
859
|
-
{onExportMarkdown}
|
|
860
|
-
modelLabel={turn.kind === 'translation'
|
|
861
|
-
? `${t('chat.translationReply')} · ${sourceLabel(turn)}`
|
|
862
|
-
: sourceLabel(turn)}
|
|
863
|
-
showInput={i === 0 && turn.kind !== 'translation'}
|
|
864
|
-
chosen={candidates && session.selectedTurnId === turn.id}
|
|
865
|
-
busy={actionBusy}
|
|
866
|
-
editable={canEdit(turn)}
|
|
867
|
-
onRetry={purpose === 'chat' && !restoreBlocked && (turn.persisted || capability !== null)
|
|
868
|
-
? () => void retryReply(turn)
|
|
869
|
-
: undefined}
|
|
870
|
-
onEdit={purpose === 'chat' && history && session.conversationId && turn.persisted
|
|
871
|
-
? () => editReply(turn)
|
|
872
|
-
: undefined}
|
|
873
|
-
onBranch={purpose === 'chat' && history && session.conversationId && turn.persisted
|
|
874
|
-
? () => void mutateReply(turn, 'branch')
|
|
875
|
-
: undefined}
|
|
876
|
-
onCopyConversation={purpose === 'chat' && history && session.conversationId && turn.persisted
|
|
877
|
-
? () => void mutateReply(turn, 'copy')
|
|
878
|
-
: undefined}
|
|
879
|
-
onSwitchModel={purpose === 'chat' && history && session.conversationId && turn.persisted
|
|
880
|
-
? (picked: ModelRef) => void switchModel(turn, picked)
|
|
881
|
-
: undefined}
|
|
882
|
-
modelOptions={turn.images.length > 0
|
|
883
|
-
? (modelOptions?.filter((option) => option.canVision) ?? null)
|
|
884
|
-
: modelOptions}
|
|
885
|
-
sourceModel={sourceModel(turn)}
|
|
886
|
-
onTranslate={purpose === 'chat' && history && session.conversationId && turn.persisted
|
|
887
|
-
? () => translateDialog(turn)
|
|
888
|
-
: undefined}
|
|
889
|
-
onDelete={purpose === 'chat' && history && session.conversationId && turn.persisted
|
|
890
|
-
? () => void mutateReply(turn, 'remove')
|
|
891
|
-
: undefined}
|
|
892
|
-
onUseContext={candidates && purpose === 'chat' && history && session.conversationId && turn.persisted
|
|
893
|
-
? () => void mutateReply(turn, 'select')
|
|
894
|
-
: undefined}
|
|
895
|
-
onApprove={(tool, allow) => session.approve(tool, allow)}
|
|
896
|
-
/>
|
|
897
|
-
{/each}
|
|
898
|
-
{/each}
|
|
899
|
-
</ol>
|
|
900
|
-
{/if}
|
|
901
|
-
{#if modelError}
|
|
902
|
-
<Alert.Root variant="destructive">
|
|
903
|
-
<Alert.Title>{t('chat.modelsFailed')}</Alert.Title>
|
|
904
|
-
<Alert.Description
|
|
905
|
-
>{modelError}<Button variant="outline" size="sm" onclick={() => modelReload++}
|
|
906
|
-
>{t('chat.retryModels')}</Button
|
|
907
|
-
></Alert.Description
|
|
908
|
-
>
|
|
909
|
-
</Alert.Root>
|
|
910
|
-
{:else if modelOptions === null}
|
|
911
|
-
<p class="empty-message" role="status">{t('chat.modelsLoading')}</p>
|
|
912
|
-
{:else if noModels}
|
|
913
|
-
<Empty.Root
|
|
914
|
-
><Empty.Description>{t('chat.noModels')}</Empty.Description>{#if onOpenSettings}<Button
|
|
915
|
-
variant="outline"
|
|
916
|
-
size="sm"
|
|
917
|
-
onclick={onOpenSettings}>{t('chat.openSettings')}</Button
|
|
918
|
-
>{/if}</Empty.Root
|
|
919
|
-
>
|
|
920
|
-
{:else if session.turns.length === 0 && !restoreBlocked}
|
|
921
|
-
<p class="empty-message">{t('chat.empty')}</p>
|
|
922
|
-
{/if}
|
|
923
|
-
</div>
|
|
924
|
-
{#if !follow && session.turns.length > 0}
|
|
925
|
-
<div class="bottom-control">
|
|
926
|
-
<Button type="button" variant="secondary" size="sm" onclick={toBottom}
|
|
927
|
-
><ArrowDownIcon data-icon="inline-start" />{t('chat.backToBottom')}</Button
|
|
928
|
-
>
|
|
929
|
-
</div>
|
|
930
|
-
{/if}
|
|
931
|
-
<form
|
|
932
|
-
class="composer"
|
|
933
|
-
class:expanded
|
|
934
|
-
onsubmit={(e) => {
|
|
935
|
-
e.preventDefault()
|
|
936
|
-
void send()
|
|
937
|
-
}}
|
|
938
|
-
ondragover={(e) => {
|
|
939
|
-
if (e.dataTransfer?.types.includes('Files')) e.preventDefault()
|
|
940
|
-
}}
|
|
941
|
-
ondrop={(e) => {
|
|
942
|
-
if (!hasImages(e.dataTransfer?.files)) return
|
|
943
|
-
e.preventDefault()
|
|
944
|
-
void attach(e.dataTransfer?.files)
|
|
945
|
-
}}
|
|
343
|
+
<section
|
|
344
|
+
class="flex min-h-0 min-w-0 flex-1 flex-col gap-3 h-full [container-type:size]"
|
|
345
|
+
aria-label={t('chat.title')}
|
|
346
|
+
{onkeydown}
|
|
347
|
+
>
|
|
348
|
+
<ChatSurface
|
|
349
|
+
label={t('chat.title')}
|
|
350
|
+
messagesLabel={t('chat.messages')}
|
|
351
|
+
latestLabel={t('chat.backToBottom')}
|
|
352
|
+
{followKey}
|
|
353
|
+
scrollKey={`${session.conversationId ?? ''}:${scrollRevision}`}
|
|
946
354
|
>
|
|
947
|
-
|
|
948
|
-
<
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
355
|
+
{#snippet header()}
|
|
356
|
+
<ChatPanelHeader
|
|
357
|
+
{i18n}
|
|
358
|
+
title={historyState.title}
|
|
359
|
+
{session}
|
|
360
|
+
{history}
|
|
361
|
+
historyBusy={historyState.historyBusy}
|
|
362
|
+
historyOpen={historyState.historyOpen}
|
|
363
|
+
historyError={historyState.historyError}
|
|
364
|
+
onHistory={() => {
|
|
365
|
+
historyState.historyOpen = true
|
|
366
|
+
void historyState.refresh()
|
|
958
367
|
}}
|
|
959
|
-
|
|
960
|
-
{
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
{/each}
|
|
992
|
-
</ul>
|
|
993
|
-
{#if !vision}<p class="search-status" role="status">{t('chat.visionUnsupported')}</p>{/if}
|
|
994
|
-
{/if}
|
|
995
|
-
<Textarea
|
|
996
|
-
bind:value={draft}
|
|
997
|
-
rows={3}
|
|
998
|
-
aria-label={t('chat.input')}
|
|
999
|
-
placeholder={t('chat.placeholder')}
|
|
1000
|
-
oninput={() => draftRevision++}
|
|
1001
|
-
onpaste={(e) => {
|
|
1002
|
-
const files = e.clipboardData?.files
|
|
1003
|
-
if (!hasImages(files)) return
|
|
1004
|
-
e.preventDefault()
|
|
1005
|
-
void attach(files)
|
|
1006
|
-
}}
|
|
1007
|
-
onkeydown={(e) => {
|
|
1008
|
-
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && !e.isComposing && e.keyCode !== 229) {
|
|
1009
|
-
e.preventDefault()
|
|
1010
|
-
if (!e.repeat) void send()
|
|
1011
|
-
}
|
|
1012
|
-
}}
|
|
368
|
+
{onClose}
|
|
369
|
+
onRetryHistory={() => void historyState.retryHistory()}
|
|
370
|
+
/>
|
|
371
|
+
{/snippet}
|
|
372
|
+
<ChatPanelMessages
|
|
373
|
+
{i18n}
|
|
374
|
+
{session}
|
|
375
|
+
groups={historyState.groups}
|
|
376
|
+
{purpose}
|
|
377
|
+
{history}
|
|
378
|
+
restoreBlocked={historyState.restoreBlocked}
|
|
379
|
+
actionBusy={historyState.actionBusy}
|
|
380
|
+
{modelOptions}
|
|
381
|
+
{modelError}
|
|
382
|
+
{capability}
|
|
383
|
+
{noModels}
|
|
384
|
+
onRetryModels={() => void models.load()}
|
|
385
|
+
{onOpenSettings}
|
|
386
|
+
{onInsert}
|
|
387
|
+
{onSaveNote}
|
|
388
|
+
{onExportMarkdown}
|
|
389
|
+
sourceLabel={historyState.sourceLabel}
|
|
390
|
+
sourceModel={historyState.sourceModel}
|
|
391
|
+
canEdit={historyState.canEdit}
|
|
392
|
+
onRetry={(turn) => void historyState.retryReply(turn)}
|
|
393
|
+
onEdit={historyState.editReply}
|
|
394
|
+
onBranch={(turn) => void historyState.mutateReply(turn, 'branch')}
|
|
395
|
+
onCopyConversation={(turn) => void historyState.mutateReply(turn, 'copy')}
|
|
396
|
+
onSwitchModel={(turn, next) => void historyState.switchModel(turn, next)}
|
|
397
|
+
onTranslate={historyState.translateDialog}
|
|
398
|
+
onDelete={(turn) => void historyState.mutateReply(turn, 'remove')}
|
|
399
|
+
onUseContext={(turn) => void historyState.mutateReply(turn, 'select')}
|
|
1013
400
|
/>
|
|
1014
|
-
{#
|
|
1015
|
-
<
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
{
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
{
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
{
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
type="button"
|
|
1041
|
-
variant="ghost"
|
|
1042
|
-
size="icon-sm"
|
|
1043
|
-
disabled={session.running || historyBusy}
|
|
1044
|
-
aria-label={t('chat.newConversation')}
|
|
1045
|
-
title={t('chat.newConversation')}
|
|
1046
|
-
onclick={newConversation}>{@render toolIcon(id)}</Button
|
|
1047
|
-
>{:else if id === 'web-search'}<Button
|
|
1048
|
-
type="button"
|
|
1049
|
-
variant="ghost"
|
|
1050
|
-
class="search-toggle"
|
|
1051
|
-
size="icon-sm"
|
|
1052
|
-
aria-label={t('chat.network')}
|
|
1053
|
-
aria-pressed={webSearch}
|
|
1054
|
-
disabled={session.running}
|
|
1055
|
-
title={`${t(webSearch ? 'chat.networkOn' : 'chat.networkOff')} · ${t('chat.networkPrivacy')}`}
|
|
1056
|
-
onclick={() => (webSearch = !webSearch)}>{@render toolIcon(id)}</Button
|
|
1057
|
-
>{:else if id === 'attachment'}<Button
|
|
1058
|
-
type="button"
|
|
1059
|
-
variant="ghost"
|
|
1060
|
-
size="icon-sm"
|
|
1061
|
-
aria-label={t('chat.uploadAttachment')}
|
|
1062
|
-
disabled={session.running || !vision}
|
|
1063
|
-
title={vision ? t('chat.uploadAttachment') : t('chat.visionUnsupported')}
|
|
1064
|
-
onclick={() => fileInput?.click()}>{@render toolIcon(id)}</Button
|
|
1065
|
-
>{:else}<Button
|
|
1066
|
-
type="button"
|
|
1067
|
-
variant="ghost"
|
|
1068
|
-
class="search-toggle"
|
|
1069
|
-
size="icon-sm"
|
|
1070
|
-
aria-label={t('chat.generateImage')}
|
|
1071
|
-
aria-pressed={generateImage}
|
|
1072
|
-
disabled={session.running || !paintingModel || capability?.supportsTools === false}
|
|
1073
|
-
title={!paintingModel
|
|
1074
|
-
? t('chat.paintingUnconfigured')
|
|
1075
|
-
: capability?.supportsTools === false
|
|
1076
|
-
? t('chat.toolsUnsupported')
|
|
1077
|
-
: t('chat.generateImage')}
|
|
1078
|
-
onclick={() => (generateImage = !generateImage)}>{@render toolIcon(id)}</Button
|
|
1079
|
-
>{/if}
|
|
1080
|
-
{/each}
|
|
1081
|
-
<input
|
|
1082
|
-
bind:this={fileInput}
|
|
1083
|
-
type="file"
|
|
1084
|
-
accept={IMAGE_MIMES.join(',')}
|
|
1085
|
-
multiple
|
|
1086
|
-
hidden
|
|
1087
|
-
onchange={(e) => {
|
|
1088
|
-
const input = e.currentTarget
|
|
1089
|
-
void attach(input.files).finally(() => (input.value = ''))
|
|
1090
|
-
}}
|
|
1091
|
-
/>
|
|
1092
|
-
<Popover.Root bind:open={toolsOpen} onOpenChange={(open) => !open && (customizing = false)}>
|
|
1093
|
-
<Popover.Trigger disabled={session.running}>
|
|
1094
|
-
{#snippet child({ props })}<Button
|
|
1095
|
-
{...props}
|
|
1096
|
-
type="button"
|
|
1097
|
-
variant="ghost"
|
|
1098
|
-
size="icon-sm"
|
|
1099
|
-
aria-label={t('chat.tools')}
|
|
1100
|
-
title={t('chat.tools')}><PlusIcon /></Button
|
|
1101
|
-
>{/snippet}
|
|
1102
|
-
</Popover.Trigger>
|
|
1103
|
-
<Popover.Content class="tools-menu" side="top" align="start" sideOffset={6} collisionPadding={8}>
|
|
1104
|
-
{#if customizing}
|
|
1105
|
-
<p class="tools-title" id="{reasoningId}-toolbar">{t('chat.toolbarCustomize')}</p>
|
|
1106
|
-
<ul class="tools-order" aria-labelledby="{reasoningId}-toolbar">
|
|
1107
|
-
{#each toolOrder as id, index (id)}
|
|
1108
|
-
<li class="tools-row">
|
|
1109
|
-
<button
|
|
1110
|
-
type="button"
|
|
1111
|
-
class="tools-grip"
|
|
1112
|
-
aria-label={t('chat.toolbarDrag', { name: t(TOOL_LABEL[id] ?? id) })}
|
|
1113
|
-
onpointerdown={(e) => dragTool(e, id)}
|
|
1114
|
-
onkeydown={(e) => {
|
|
1115
|
-
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return
|
|
1116
|
-
e.preventDefault()
|
|
1117
|
-
const button = e.currentTarget
|
|
1118
|
-
moveTool(id, index + (e.key === 'ArrowUp' ? -1 : 1))
|
|
1119
|
-
void tick().then(() => button.focus())
|
|
1120
|
-
}}><GripVerticalIcon /></button
|
|
1121
|
-
>
|
|
1122
|
-
{@render toolIcon(id)}<span>{t(TOOL_LABEL[id] ?? id)}</span>
|
|
1123
|
-
<Switch
|
|
1124
|
-
size="sm"
|
|
1125
|
-
aria-label={t(TOOL_LABEL[id] ?? id)}
|
|
1126
|
-
checked={pinned.includes(id)}
|
|
1127
|
-
onCheckedChange={(on) =>
|
|
1128
|
-
setPinned(toolOrder.filter((item) => (item === id ? on : pinned.includes(item))))}
|
|
1129
|
-
/>
|
|
1130
|
-
</li>
|
|
1131
|
-
{/each}
|
|
1132
|
-
</ul>
|
|
1133
|
-
<button
|
|
1134
|
-
type="button"
|
|
1135
|
-
class="tools-item tools-footer"
|
|
1136
|
-
disabled={pinned.join() === DEFAULT_TOOLBAR.join()}
|
|
1137
|
-
onclick={() => {
|
|
1138
|
-
pinned = [...DEFAULT_TOOLBAR]
|
|
1139
|
-
onToolbarChange?.(pinned)
|
|
1140
|
-
}}><RotateCcwIcon /><span>{t('chat.toolbarRestore')}</span></button
|
|
1141
|
-
>
|
|
1142
|
-
{:else}
|
|
1143
|
-
{#each toolOrder.filter((id) => !pinned.includes(id)) as id (id)}
|
|
1144
|
-
{#if id === 'new-conversation'}<button
|
|
1145
|
-
type="button"
|
|
1146
|
-
class="tools-item"
|
|
1147
|
-
disabled={historyBusy}
|
|
1148
|
-
onclick={() => {
|
|
1149
|
-
toolsOpen = false
|
|
1150
|
-
newConversation()
|
|
1151
|
-
}}>{@render toolIcon(id)}<span>{t('chat.newConversation')}</span></button
|
|
1152
|
-
>{:else if id === 'web-search'}<button
|
|
1153
|
-
type="button"
|
|
1154
|
-
class="tools-item"
|
|
1155
|
-
aria-pressed={webSearch}
|
|
1156
|
-
onclick={() => {
|
|
1157
|
-
toolsOpen = false
|
|
1158
|
-
webSearch = !webSearch
|
|
1159
|
-
}}
|
|
1160
|
-
>{@render toolIcon(id)}<span>{t('chat.network')}</span>{#if webSearch}<CheckIcon />{/if}</button
|
|
1161
|
-
>{:else if id === 'attachment'}<button
|
|
1162
|
-
type="button"
|
|
1163
|
-
class="tools-item"
|
|
1164
|
-
disabled={!vision}
|
|
1165
|
-
title={vision ? undefined : t('chat.visionUnsupported')}
|
|
1166
|
-
onclick={() => {
|
|
1167
|
-
toolsOpen = false
|
|
1168
|
-
fileInput?.click()
|
|
1169
|
-
}}
|
|
1170
|
-
>{@render toolIcon(id)}<span>{t('chat.uploadAttachment')}</span>{#if !vision}<small
|
|
1171
|
-
>{t('chat.visionUnsupported')}</small
|
|
1172
|
-
>{/if}</button
|
|
1173
|
-
>{:else}<button
|
|
1174
|
-
type="button"
|
|
1175
|
-
class="tools-item"
|
|
1176
|
-
aria-pressed={generateImage}
|
|
1177
|
-
disabled={!paintingModel || capability?.supportsTools === false}
|
|
1178
|
-
title={!paintingModel
|
|
1179
|
-
? t('chat.paintingUnconfigured')
|
|
1180
|
-
: capability?.supportsTools === false
|
|
1181
|
-
? t('chat.toolsUnsupported')
|
|
1182
|
-
: undefined}
|
|
1183
|
-
onclick={() => {
|
|
1184
|
-
toolsOpen = false
|
|
1185
|
-
generateImage = !generateImage
|
|
1186
|
-
}}
|
|
1187
|
-
>{@render toolIcon(id)}<span>{t('chat.generateImage')}</span>{#if !paintingModel}<small
|
|
1188
|
-
>{t('chat.paintingUnconfigured')}</small
|
|
1189
|
-
>{:else if generateImage}<CheckIcon />{/if}</button
|
|
1190
|
-
>{/if}
|
|
1191
|
-
{/each}
|
|
1192
|
-
<button
|
|
1193
|
-
type="button"
|
|
1194
|
-
class="tools-item tools-footer"
|
|
1195
|
-
aria-label={t('chat.toolbarCustomize')}
|
|
1196
|
-
onclick={() => (customizing = true)}><span>{t('chat.quickPanel')}</span><Settings2Icon /></button
|
|
1197
|
-
>
|
|
1198
|
-
{/if}
|
|
1199
|
-
</Popover.Content>
|
|
1200
|
-
</Popover.Root>
|
|
1201
|
-
{#if generateImage && paintingModel && purpose === 'chat' && !pinned.includes('generate-image')}<Button
|
|
1202
|
-
type="button"
|
|
1203
|
-
variant="secondary"
|
|
1204
|
-
size="sm"
|
|
1205
|
-
class="tool-chip"
|
|
1206
|
-
aria-label={t('chat.generateImageOff')}
|
|
1207
|
-
title={t('chat.generateImageOff')}
|
|
1208
|
-
onclick={() => (generateImage = false)}><ImageIcon />{t('chat.generateImage')}<XIcon /></Button
|
|
1209
|
-
>{/if}
|
|
1210
|
-
{#if ports.skills}<div class="skills-control">
|
|
1211
|
-
<SkillPicker
|
|
1212
|
-
port={ports.skills}
|
|
1213
|
-
value={selected}
|
|
1214
|
-
budget={skillsBudget}
|
|
1215
|
-
changed={session.skillChanged}
|
|
1216
|
-
onchange={setSkills}
|
|
1217
|
-
onresolved={() => (session.skillChanged = null)}
|
|
1218
|
-
/>
|
|
1219
|
-
</div>{/if}
|
|
1220
|
-
</div>
|
|
1221
|
-
<div class="tool-group send-tools">
|
|
1222
|
-
{#if mode === 'plan'}<span class="plan-badge" role="status">{t('chat.mode.planBadge')}</span>{/if}
|
|
1223
|
-
{#if allowedModes.length > 1 || allowedModes.includes('full_access')}
|
|
1224
|
-
<DropdownMenu.Root>
|
|
1225
|
-
<DropdownMenu.Trigger disabled={session.running}>
|
|
1226
|
-
{#snippet child({ props })}<Button
|
|
1227
|
-
{...props}
|
|
1228
|
-
variant="ghost"
|
|
1229
|
-
size="sm"
|
|
1230
|
-
class={mode === 'full_access' ? 'mode-danger' : undefined}
|
|
1231
|
-
aria-label={`${t('chat.mode.label')}: ${t(`chat.mode.${mode}`)}`}
|
|
1232
|
-
><ShieldIcon data-icon="inline-start" />{t(`chat.mode.${mode}`)}<ChevronDownIcon
|
|
1233
|
-
data-icon="inline-end"
|
|
1234
|
-
/></Button
|
|
1235
|
-
>{/snippet}
|
|
1236
|
-
</DropdownMenu.Trigger>
|
|
1237
|
-
<DropdownMenu.Content side="top" align="end" class="mode-menu">
|
|
1238
|
-
<DropdownMenu.Group aria-label={t('chat.mode.label')}>
|
|
1239
|
-
{#each allowedModes as option (option)}
|
|
1240
|
-
<DropdownMenu.Item
|
|
1241
|
-
class={option === 'full_access' ? 'mode-option mode-danger' : 'mode-option'}
|
|
1242
|
-
aria-current={option === mode ? 'true' : undefined}
|
|
1243
|
-
onSelect={() => void chooseMode(option)}
|
|
1244
|
-
>{#if option === 'default'}<HandIcon />
|
|
1245
|
-
{:else if option === 'plan'}<RouteIcon />
|
|
1246
|
-
{:else if option === 'accept_edits'}<FilePenIcon />
|
|
1247
|
-
{:else if option === 'auto'}<ShieldCheckIcon />
|
|
1248
|
-
{:else}<ShieldAlertIcon />{/if}
|
|
1249
|
-
<span class="mode-name">{t(`chat.mode.${option}`)}</span>
|
|
1250
|
-
<span class="mode-description">{t(`chat.mode.${option}Hint`)}</span>
|
|
1251
|
-
<span class="mode-notice">
|
|
1252
|
-
{#if option === 'auto' || option === 'full_access'}
|
|
1253
|
-
<span title={t(option === 'auto' ? 'chat.mode.ruleNotice' : 'chat.mode.fullAccessBody')}>
|
|
1254
|
-
<InfoIcon />
|
|
1255
|
-
</span>
|
|
1256
|
-
{/if}
|
|
1257
|
-
</span>
|
|
1258
|
-
<span class="mode-check"
|
|
1259
|
-
>{#if option === mode}<CheckIcon />{/if}</span
|
|
1260
|
-
></DropdownMenu.Item
|
|
1261
|
-
>
|
|
1262
|
-
{/each}
|
|
1263
|
-
</DropdownMenu.Group>
|
|
1264
|
-
<div class="mode-footer">
|
|
1265
|
-
<span>{t('chat.mode.label')}</span>
|
|
1266
|
-
<span class="mode-keys"
|
|
1267
|
-
><kbd>↑↓</kbd>{t('chat.mode.navigate')}<kbd>↵</kbd>{t('chat.mode.confirm')}<kbd>Esc</kbd>{t(
|
|
1268
|
-
'chat.mode.close',
|
|
1269
|
-
)}</span
|
|
1270
|
-
>
|
|
1271
|
-
</div>
|
|
1272
|
-
</DropdownMenu.Content>
|
|
1273
|
-
</DropdownMenu.Root>
|
|
1274
|
-
{/if}
|
|
1275
|
-
{#if picked}
|
|
1276
|
-
<Popover.Root bind:open={reasoningOpen}>
|
|
1277
|
-
<Popover.Trigger disabled={session.running}>
|
|
1278
|
-
{#snippet child({ props })}
|
|
1279
|
-
<Button
|
|
1280
|
-
{...props}
|
|
1281
|
-
bind:ref={reasoningTrigger}
|
|
1282
|
-
variant="ghost"
|
|
1283
|
-
size="sm"
|
|
1284
|
-
aria-label={`${t('chat.reasoningEffort')}: ${effortLabel}`}
|
|
1285
|
-
>
|
|
1286
|
-
<GaugeIcon data-icon="inline-start" />
|
|
1287
|
-
{effortLabel}
|
|
1288
|
-
<ChevronDownIcon data-icon="inline-end" />
|
|
1289
|
-
</Button>
|
|
1290
|
-
{/snippet}
|
|
1291
|
-
</Popover.Trigger>
|
|
1292
|
-
<Popover.Content
|
|
1293
|
-
class="reasoning-popover"
|
|
1294
|
-
side="top"
|
|
1295
|
-
align="end"
|
|
1296
|
-
sideOffset={8}
|
|
1297
|
-
collisionPadding={8}
|
|
1298
|
-
trapFocus={false}
|
|
1299
|
-
preventScroll={false}
|
|
1300
|
-
aria-label={t('chat.reasoningEffort')}
|
|
1301
|
-
onEscapeKeydown={(event) => {
|
|
1302
|
-
if (event.isComposing) event.preventDefault()
|
|
1303
|
-
event.stopPropagation()
|
|
1304
|
-
}}
|
|
1305
|
-
onCloseAutoFocus={(event) => {
|
|
1306
|
-
event.preventDefault()
|
|
1307
|
-
reasoningTrigger?.focus({ preventScroll: true })
|
|
1308
|
-
}}
|
|
1309
|
-
>
|
|
1310
|
-
<div class="reasoning-heading">
|
|
1311
|
-
<p>{t('chat.reasoningStrength')}: <strong>{effortLabel}</strong></p>
|
|
1312
|
-
<Button
|
|
1313
|
-
variant="secondary"
|
|
1314
|
-
size="xs"
|
|
1315
|
-
disabled={session.running || chosenEffort === 'default'}
|
|
1316
|
-
onclick={() => chooseEffort('default')}>{t('chat.resetReasoning')}</Button
|
|
1317
|
-
>
|
|
1318
|
-
</div>
|
|
1319
|
-
{#if allowedEfforts.length === 0}
|
|
1320
|
-
<p class="reasoning-hint">{t('chat.reasoningUnconfigured')}</p>
|
|
1321
|
-
{#if onOpenSettings}<Button
|
|
1322
|
-
variant="outline"
|
|
1323
|
-
size="sm"
|
|
1324
|
-
onclick={() => {
|
|
1325
|
-
reasoningOpen = false
|
|
1326
|
-
settingsTarget.value = picked
|
|
1327
|
-
? { endpointId: picked.endpointId, modelId: picked.modelId }
|
|
1328
|
-
: null
|
|
1329
|
-
onOpenSettings()
|
|
1330
|
-
}}>{t('chat.openSettings')}</Button
|
|
1331
|
-
>{/if}
|
|
1332
|
-
{:else if allowedEfforts.length > 1}
|
|
1333
|
-
<div class="reasoning-scale" aria-hidden="true">
|
|
1334
|
-
<span>{t('chat.reasoningFaster')}</span><span>{t('chat.reasoningSmarter')}</span>
|
|
1335
|
-
</div>
|
|
1336
|
-
<div
|
|
1337
|
-
class="reasoning-slider"
|
|
1338
|
-
style:--reasoning-progress={`${(effortIndex / (allowedEfforts.length - 1)) * 100}%`}
|
|
1339
|
-
>
|
|
1340
|
-
<input
|
|
1341
|
-
type="range"
|
|
1342
|
-
min="0"
|
|
1343
|
-
max={allowedEfforts.length - 1}
|
|
1344
|
-
step="1"
|
|
1345
|
-
value={effortIndex}
|
|
1346
|
-
aria-label={t('chat.reasoningEffort')}
|
|
1347
|
-
aria-valuetext={effortLabel}
|
|
1348
|
-
disabled={session.running}
|
|
1349
|
-
oninput={commitReasoning}
|
|
1350
|
-
onpointerup={(event) => {
|
|
1351
|
-
if (event.button === 0) commitReasoning(event)
|
|
1352
|
-
}}
|
|
1353
|
-
onkeyup={(event) => {
|
|
1354
|
-
if (
|
|
1355
|
-
!event.isComposing &&
|
|
1356
|
-
['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)
|
|
1357
|
-
) {
|
|
1358
|
-
commitReasoning(event)
|
|
1359
|
-
}
|
|
1360
|
-
}}
|
|
1361
|
-
/>
|
|
1362
|
-
<div class="reasoning-ticks" aria-hidden="true">
|
|
1363
|
-
{#each allowedEfforts as effort, index (effort)}
|
|
1364
|
-
<span
|
|
1365
|
-
class="reasoning-tick"
|
|
1366
|
-
data-effort={effort}
|
|
1367
|
-
class:current={index === effortIndex}
|
|
1368
|
-
style:left={`${(index / (allowedEfforts.length - 1)) * 100}%`}
|
|
1369
|
-
></span>
|
|
1370
|
-
{/each}
|
|
1371
|
-
</div>
|
|
1372
|
-
</div>
|
|
1373
|
-
{:else}
|
|
1374
|
-
<Field.FieldSet disabled={session.running}>
|
|
1375
|
-
<Field.FieldLegend class="sr-only">{t('chat.reasoningEffort')}</Field.FieldLegend>
|
|
1376
|
-
{#each ['default', ...allowedEfforts] as effort (effort)}
|
|
1377
|
-
<label class="reasoning-option">
|
|
1378
|
-
<input
|
|
1379
|
-
type="radio"
|
|
1380
|
-
name={reasoningId}
|
|
1381
|
-
value={effort}
|
|
1382
|
-
checked={reasoningEffort === effort}
|
|
1383
|
-
onchange={() => chooseEffort(effort as ReasoningEffort)}
|
|
1384
|
-
/>
|
|
1385
|
-
{t(`chat.effort.${effort}`)}
|
|
1386
|
-
</label>
|
|
1387
|
-
{/each}
|
|
1388
|
-
</Field.FieldSet>
|
|
1389
|
-
{/if}
|
|
1390
|
-
{#if reasoningEffort === 'default'}<p class="reasoning-hint">{t('chat.reasoningDefaultHint')}</p>{/if}
|
|
1391
|
-
</Popover.Content>
|
|
1392
|
-
</Popover.Root>
|
|
1393
|
-
{/if}
|
|
1394
|
-
<UsageDetails
|
|
1395
|
-
context={currentContext}
|
|
1396
|
-
usage={session.turns.at(-1)?.usage ?? null}
|
|
1397
|
-
total={session.cumulativeUsage}
|
|
1398
|
-
cost={session.turns.at(-1)?.cost ?? null}
|
|
1399
|
-
totalCost={session.cumulativeCost}
|
|
1400
|
-
/>
|
|
1401
|
-
{#if session.running}<Button
|
|
1402
|
-
type="button"
|
|
1403
|
-
variant="destructive"
|
|
1404
|
-
size="icon-sm"
|
|
1405
|
-
aria-label={t('chat.stop')}
|
|
1406
|
-
title={t('chat.stop')}
|
|
1407
|
-
onclick={() => session.stop()}><SquareIcon /></Button
|
|
1408
|
-
>
|
|
1409
|
-
{:else}<Button
|
|
1410
|
-
type="submit"
|
|
1411
|
-
size="icon-sm"
|
|
1412
|
-
disabled={!canSend}
|
|
1413
|
-
aria-label={t('chat.send')}
|
|
1414
|
-
title={t('chat.send')}><ArrowUpIcon /></Button
|
|
1415
|
-
>{/if}
|
|
1416
|
-
</div>
|
|
1417
|
-
</div>
|
|
1418
|
-
</form>
|
|
1419
|
-
{#if history}
|
|
1420
|
-
<Dialog.Root bind:open={historyOpen}>
|
|
1421
|
-
<Dialog.Content showCloseButton={false}>
|
|
1422
|
-
<Dialog.Header><Dialog.Title>{t('chat.history')}</Dialog.Title></Dialog.Header>
|
|
1423
|
-
{#if historyLoading}<p role="status">{t('chat.historyLoading')}</p>{/if}
|
|
1424
|
-
{#if historyError}<p role="alert">
|
|
1425
|
-
{t('chat.historyFailed')}: {historyError}<Button
|
|
1426
|
-
variant="outline"
|
|
1427
|
-
size="sm"
|
|
1428
|
-
disabled={historyBusy}
|
|
1429
|
-
onclick={() => void retryHistory()}>{t('chat.retryHistory')}</Button
|
|
1430
|
-
>
|
|
1431
|
-
</p>{/if}
|
|
1432
|
-
<div class="history-list">
|
|
1433
|
-
{#each conversations as c (c.id)}<Button
|
|
1434
|
-
variant="ghost"
|
|
1435
|
-
title={c.title}
|
|
1436
|
-
disabled={historyBusy || session.running}
|
|
1437
|
-
onclick={() => void open(c.id)}><span>{c.title}</span></Button
|
|
1438
|
-
>{/each}
|
|
1439
|
-
</div>
|
|
1440
|
-
{#if session.conversationId}
|
|
1441
|
-
{#if renaming}
|
|
1442
|
-
<form
|
|
1443
|
-
onsubmit={(e) => {
|
|
1444
|
-
e.preventDefault()
|
|
1445
|
-
void rename()
|
|
1446
|
-
}}
|
|
1447
|
-
>
|
|
1448
|
-
<Field.FieldGroup
|
|
1449
|
-
><Field.Field
|
|
1450
|
-
><Field.FieldLabel for="chat-conversation-title">{t('chat.conversationTitle')}</Field.FieldLabel
|
|
1451
|
-
><Input id="chat-conversation-title" bind:value={renameTitle} disabled={historyBusy} /></Field.Field
|
|
1452
|
-
><Button type="submit" disabled={historyBusy || renameTitle.trim() === ''}
|
|
1453
|
-
>{t('common.confirm')}</Button
|
|
1454
|
-
></Field.FieldGroup
|
|
1455
|
-
>
|
|
1456
|
-
</form>
|
|
1457
|
-
{:else}<Button
|
|
1458
|
-
variant="outline"
|
|
1459
|
-
disabled={historyBusy || session.running}
|
|
1460
|
-
onclick={() => {
|
|
1461
|
-
renameTitle = title
|
|
1462
|
-
renaming = true
|
|
1463
|
-
}}>{t('chat.rename')}</Button
|
|
1464
|
-
>{/if}
|
|
1465
|
-
<Button variant="destructive" disabled={historyBusy || session.running} onclick={() => void remove()}
|
|
1466
|
-
>{t('chat.delete')}</Button
|
|
1467
|
-
>
|
|
1468
|
-
{/if}
|
|
1469
|
-
<Dialog.Footer
|
|
1470
|
-
><Button variant="ghost" onclick={() => (historyOpen = false)}>{t('chat.close')}</Button></Dialog.Footer
|
|
1471
|
-
>
|
|
1472
|
-
</Dialog.Content>
|
|
1473
|
-
</Dialog.Root>
|
|
1474
|
-
{/if}
|
|
1475
|
-
<Dialog.Root bind:open={replyOpen}>
|
|
1476
|
-
<Dialog.Content class="reply-dialog" showCloseButton={false}>
|
|
1477
|
-
<Dialog.Header>
|
|
1478
|
-
<Dialog.Title>{t(replyDialog === 'edit' ? 'chat.editReply' : 'chat.translate')}</Dialog.Title>
|
|
1479
|
-
<Dialog.Description
|
|
1480
|
-
>{t(replyDialog === 'edit' ? 'chat.editReplyHint' : 'chat.candidateHint')}</Dialog.Description
|
|
1481
|
-
>
|
|
1482
|
-
</Dialog.Header>
|
|
1483
|
-
<form
|
|
1484
|
-
onsubmit={(event) => {
|
|
1485
|
-
event.preventDefault()
|
|
1486
|
-
void submitReply()
|
|
401
|
+
{#snippet composer()}
|
|
402
|
+
<ChatPanelComposer
|
|
403
|
+
{i18n}
|
|
404
|
+
{session}
|
|
405
|
+
{purpose}
|
|
406
|
+
bind:draft
|
|
407
|
+
{canSend}
|
|
408
|
+
{activeContext}
|
|
409
|
+
images={attachments.images}
|
|
410
|
+
{vision}
|
|
411
|
+
{recovery}
|
|
412
|
+
bind:webSearch
|
|
413
|
+
bind:generateImage
|
|
414
|
+
toolbar={toolbarProp}
|
|
415
|
+
{onToolbarChange}
|
|
416
|
+
{paintingModel}
|
|
417
|
+
{capability}
|
|
418
|
+
historyBusy={historyState.historyBusy}
|
|
419
|
+
onNewConversation={historyState.newConversation}
|
|
420
|
+
{attach}
|
|
421
|
+
onDraftInput={() => draftRevision++}
|
|
422
|
+
onRemoveImage={removeImage}
|
|
423
|
+
onRecoverDraft={recoverDraft}
|
|
424
|
+
onDropContext={() => {
|
|
425
|
+
droppedKey = contextKey
|
|
426
|
+
contextRevision++
|
|
1487
427
|
}}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
428
|
+
hasSearch={!!ports.search}
|
|
429
|
+
{selected}
|
|
430
|
+
{skillsBudget}
|
|
431
|
+
skillsPort={isAgent ? ports.skills : undefined}
|
|
432
|
+
onSkillsChange={setSkills}
|
|
433
|
+
onSkillResolved={() => (session.skillChanged = null)}
|
|
434
|
+
{mode}
|
|
435
|
+
{allowedModes}
|
|
436
|
+
{chooseMode}
|
|
437
|
+
bind:reasoningOpen
|
|
438
|
+
pickerModels={models.picker}
|
|
439
|
+
{pickerValue}
|
|
440
|
+
{pickerDefault}
|
|
441
|
+
pickerRefreshing={models.refreshing}
|
|
442
|
+
pickerNotice={models.notice}
|
|
443
|
+
pickerDisabled={modelError !== null}
|
|
444
|
+
onPick={pickValue}
|
|
445
|
+
onSetDefault={onDefaultModelChange
|
|
446
|
+
? (value) => void models.saveDefault(value, onDefaultModelChange)
|
|
447
|
+
: undefined}
|
|
448
|
+
onRefresh={(force) => void models.load(force)}
|
|
449
|
+
{currentContext}
|
|
450
|
+
{picked}
|
|
451
|
+
{onOpenSettings}
|
|
452
|
+
onSend={() => void send()}
|
|
453
|
+
onStop={() => session.stop()}
|
|
454
|
+
{hasImages}
|
|
455
|
+
override={pendingApprovals.length ? approvals : undefined}
|
|
456
|
+
/>
|
|
457
|
+
{/snippet}
|
|
458
|
+
</ChatSurface>
|
|
459
|
+
{#snippet approvals()}
|
|
460
|
+
<ChatPanelApprovals {session} tools={pendingApprovals} {i18n} />
|
|
461
|
+
{/snippet}
|
|
462
|
+
<ChatPanelDialogs
|
|
463
|
+
{i18n}
|
|
464
|
+
{history}
|
|
465
|
+
{session}
|
|
466
|
+
conversations={historyState.conversations}
|
|
467
|
+
title={historyState.title}
|
|
468
|
+
historyLoading={historyState.historyLoading}
|
|
469
|
+
historyBusy={historyState.historyBusy}
|
|
470
|
+
historyError={historyState.historyError}
|
|
471
|
+
retryHistory={historyState.retryHistory}
|
|
472
|
+
open={historyState.open}
|
|
473
|
+
rename={historyState.rename}
|
|
474
|
+
remove={historyState.remove}
|
|
475
|
+
actionBusy={historyState.actionBusy}
|
|
476
|
+
replyDialog={historyState.replyDialog}
|
|
477
|
+
{modelOptions}
|
|
478
|
+
{modelError}
|
|
479
|
+
{noModels}
|
|
480
|
+
validReplyModel={historyState.validReplyModel}
|
|
481
|
+
submitReply={historyState.submitReply}
|
|
482
|
+
reloadModels={() => void models.load()}
|
|
483
|
+
bind:historyOpen={historyState.historyOpen}
|
|
484
|
+
bind:renaming={historyState.renaming}
|
|
485
|
+
bind:renameTitle={historyState.renameTitle}
|
|
486
|
+
bind:replyOpen={historyState.replyOpen}
|
|
487
|
+
bind:editDraft={historyState.editDraft}
|
|
488
|
+
bind:replyModel={historyState.replyModel}
|
|
489
|
+
bind:targetLanguage={historyState.targetLanguage}
|
|
490
|
+
replyError={historyState.replyError}
|
|
491
|
+
/>
|
|
1541
492
|
</section>
|
|
1542
493
|
{/if}
|
|
1543
|
-
|
|
1544
|
-
<style>
|
|
1545
|
-
.chat-panel {
|
|
1546
|
-
display: flex;
|
|
1547
|
-
flex: 1;
|
|
1548
|
-
flex-direction: column;
|
|
1549
|
-
gap: 0.75rem;
|
|
1550
|
-
min-width: 0;
|
|
1551
|
-
min-height: 0;
|
|
1552
|
-
height: 100%;
|
|
1553
|
-
container-type: size;
|
|
1554
|
-
}
|
|
1555
|
-
.chat-header {
|
|
1556
|
-
display: flex;
|
|
1557
|
-
flex-direction: column;
|
|
1558
|
-
gap: 0.5rem;
|
|
1559
|
-
flex: none;
|
|
1560
|
-
min-width: 0;
|
|
1561
|
-
}
|
|
1562
|
-
.title-row {
|
|
1563
|
-
display: flex;
|
|
1564
|
-
align-items: center;
|
|
1565
|
-
gap: 0.25rem;
|
|
1566
|
-
min-width: 0;
|
|
1567
|
-
}
|
|
1568
|
-
.title-row h2 {
|
|
1569
|
-
flex: 1;
|
|
1570
|
-
min-width: 0;
|
|
1571
|
-
margin: 0;
|
|
1572
|
-
font: inherit;
|
|
1573
|
-
font-weight: 600;
|
|
1574
|
-
overflow: hidden;
|
|
1575
|
-
text-overflow: ellipsis;
|
|
1576
|
-
white-space: nowrap;
|
|
1577
|
-
}
|
|
1578
|
-
.title-row :global(button) {
|
|
1579
|
-
flex-shrink: 0;
|
|
1580
|
-
}
|
|
1581
|
-
.model-row {
|
|
1582
|
-
min-width: 0;
|
|
1583
|
-
}
|
|
1584
|
-
.messages {
|
|
1585
|
-
flex: 1;
|
|
1586
|
-
min-height: 0;
|
|
1587
|
-
min-width: 0;
|
|
1588
|
-
overflow-y: auto;
|
|
1589
|
-
overflow-x: clip;
|
|
1590
|
-
overscroll-behavior: contain;
|
|
1591
|
-
overflow-anchor: none;
|
|
1592
|
-
}
|
|
1593
|
-
.turns {
|
|
1594
|
-
display: flex;
|
|
1595
|
-
flex-direction: column;
|
|
1596
|
-
gap: 1rem;
|
|
1597
|
-
padding: 0;
|
|
1598
|
-
margin: 0;
|
|
1599
|
-
list-style: none;
|
|
1600
|
-
}
|
|
1601
|
-
.empty-message {
|
|
1602
|
-
color: var(--color-muted-foreground);
|
|
1603
|
-
text-align: center;
|
|
1604
|
-
padding: 1.5rem 0.5rem;
|
|
1605
|
-
margin: 0;
|
|
1606
|
-
}
|
|
1607
|
-
.bottom-control {
|
|
1608
|
-
display: flex;
|
|
1609
|
-
justify-content: center;
|
|
1610
|
-
}
|
|
1611
|
-
.composer {
|
|
1612
|
-
position: relative;
|
|
1613
|
-
flex: none;
|
|
1614
|
-
min-width: 0;
|
|
1615
|
-
display: flex;
|
|
1616
|
-
flex-direction: column;
|
|
1617
|
-
gap: 0.5rem;
|
|
1618
|
-
border: 1px solid var(--color-border);
|
|
1619
|
-
border-radius: calc(var(--kit-radius, var(--radius)) * 1.5);
|
|
1620
|
-
padding: 0.5rem;
|
|
1621
|
-
}
|
|
1622
|
-
.expand-control {
|
|
1623
|
-
position: absolute;
|
|
1624
|
-
top: 0.375rem;
|
|
1625
|
-
right: 0.375rem;
|
|
1626
|
-
}
|
|
1627
|
-
.composer :global(textarea) {
|
|
1628
|
-
min-height: 5rem;
|
|
1629
|
-
height: 5rem;
|
|
1630
|
-
max-height: 5rem;
|
|
1631
|
-
field-sizing: fixed;
|
|
1632
|
-
resize: none;
|
|
1633
|
-
padding-right: 2.25rem;
|
|
1634
|
-
border: 0;
|
|
1635
|
-
box-shadow: none;
|
|
1636
|
-
border-radius: calc(var(--kit-radius, var(--radius)) * 0.5);
|
|
1637
|
-
}
|
|
1638
|
-
.composer.expanded :global(textarea) {
|
|
1639
|
-
height: 40cqh;
|
|
1640
|
-
max-height: 40cqh;
|
|
1641
|
-
}
|
|
1642
|
-
.composer :global(textarea:focus-visible) {
|
|
1643
|
-
outline: 2px solid var(--color-ring);
|
|
1644
|
-
outline-offset: -2px;
|
|
1645
|
-
}
|
|
1646
|
-
.context-chip {
|
|
1647
|
-
display: flex;
|
|
1648
|
-
align-items: center;
|
|
1649
|
-
align-self: flex-start;
|
|
1650
|
-
gap: 0.25rem;
|
|
1651
|
-
max-width: calc(100% - 2rem);
|
|
1652
|
-
padding-left: 0.5rem;
|
|
1653
|
-
background: var(--color-muted);
|
|
1654
|
-
border-radius: var(--kit-radius, var(--radius));
|
|
1655
|
-
}
|
|
1656
|
-
.context-chip span {
|
|
1657
|
-
min-width: 0;
|
|
1658
|
-
overflow: hidden;
|
|
1659
|
-
text-overflow: ellipsis;
|
|
1660
|
-
white-space: nowrap;
|
|
1661
|
-
}
|
|
1662
|
-
.context-chip :global(button) {
|
|
1663
|
-
flex-shrink: 0;
|
|
1664
|
-
}
|
|
1665
|
-
.image-tray {
|
|
1666
|
-
display: flex;
|
|
1667
|
-
flex-wrap: wrap;
|
|
1668
|
-
gap: 0.375rem;
|
|
1669
|
-
margin: 0;
|
|
1670
|
-
padding: 0;
|
|
1671
|
-
list-style: none;
|
|
1672
|
-
}
|
|
1673
|
-
.image-tray li {
|
|
1674
|
-
position: relative;
|
|
1675
|
-
}
|
|
1676
|
-
.image-tray img {
|
|
1677
|
-
width: 3.5rem;
|
|
1678
|
-
height: 3.5rem;
|
|
1679
|
-
object-fit: cover;
|
|
1680
|
-
border: 1px solid var(--color-border);
|
|
1681
|
-
border-radius: var(--kit-radius, var(--radius));
|
|
1682
|
-
}
|
|
1683
|
-
.image-tray :global(button) {
|
|
1684
|
-
position: absolute;
|
|
1685
|
-
top: -0.375rem;
|
|
1686
|
-
right: -0.375rem;
|
|
1687
|
-
}
|
|
1688
|
-
.composer-tools,
|
|
1689
|
-
.tool-group {
|
|
1690
|
-
display: flex;
|
|
1691
|
-
align-items: center;
|
|
1692
|
-
flex-wrap: wrap;
|
|
1693
|
-
gap: 0.25rem;
|
|
1694
|
-
min-width: 0;
|
|
1695
|
-
}
|
|
1696
|
-
.composer-tools {
|
|
1697
|
-
justify-content: space-between;
|
|
1698
|
-
gap: 0.5rem;
|
|
1699
|
-
}
|
|
1700
|
-
.send-tools {
|
|
1701
|
-
margin-left: auto;
|
|
1702
|
-
}
|
|
1703
|
-
.chat-panel :global(button.search-toggle) {
|
|
1704
|
-
color: var(--color-muted-foreground);
|
|
1705
|
-
}
|
|
1706
|
-
.chat-panel :global(button.search-toggle[aria-pressed='true']) {
|
|
1707
|
-
color: var(--color-primary-foreground);
|
|
1708
|
-
background: var(--color-primary);
|
|
1709
|
-
border-color: var(--color-primary);
|
|
1710
|
-
}
|
|
1711
|
-
.chat-panel :global(button.search-toggle[aria-pressed='true']:hover:not(:disabled)) {
|
|
1712
|
-
background: color-mix(in oklch, var(--color-primary) 85%, var(--color-foreground));
|
|
1713
|
-
}
|
|
1714
|
-
:global(.tools-menu) {
|
|
1715
|
-
z-index: 50;
|
|
1716
|
-
box-sizing: border-box;
|
|
1717
|
-
width: min(16rem, calc(100vw - 1rem));
|
|
1718
|
-
display: flex;
|
|
1719
|
-
flex-direction: column;
|
|
1720
|
-
padding: 0.25rem;
|
|
1721
|
-
border: 1px solid var(--color-border);
|
|
1722
|
-
border-radius: var(--kit-radius, var(--radius));
|
|
1723
|
-
background: var(--color-popover);
|
|
1724
|
-
color: var(--color-popover-foreground);
|
|
1725
|
-
box-shadow: 0 4px 12px color-mix(in oklch, var(--color-foreground) 12%, transparent);
|
|
1726
|
-
font-size: var(--kit-font-size-sm, 0.8125rem);
|
|
1727
|
-
}
|
|
1728
|
-
:global(.tools-item) {
|
|
1729
|
-
display: flex;
|
|
1730
|
-
align-items: center;
|
|
1731
|
-
gap: 0.5rem;
|
|
1732
|
-
min-height: 2.25rem;
|
|
1733
|
-
padding: 0.25rem 0.625rem;
|
|
1734
|
-
border: 0;
|
|
1735
|
-
border-radius: calc(var(--kit-radius, var(--radius)) - 2px);
|
|
1736
|
-
background: transparent;
|
|
1737
|
-
color: inherit;
|
|
1738
|
-
font: inherit;
|
|
1739
|
-
text-align: left;
|
|
1740
|
-
cursor: pointer;
|
|
1741
|
-
}
|
|
1742
|
-
:global(.tools-item:hover:not(:disabled)),
|
|
1743
|
-
:global(.tools-item:focus-visible) {
|
|
1744
|
-
background: var(--color-accent);
|
|
1745
|
-
color: var(--color-accent-foreground);
|
|
1746
|
-
outline: none;
|
|
1747
|
-
}
|
|
1748
|
-
:global(.tools-item:disabled) {
|
|
1749
|
-
cursor: not-allowed;
|
|
1750
|
-
opacity: 0.55;
|
|
1751
|
-
}
|
|
1752
|
-
:global(.tools-item svg) {
|
|
1753
|
-
width: 1rem;
|
|
1754
|
-
height: 1rem;
|
|
1755
|
-
flex-shrink: 0;
|
|
1756
|
-
}
|
|
1757
|
-
:global(.tools-item small) {
|
|
1758
|
-
margin-left: auto;
|
|
1759
|
-
color: var(--color-muted-foreground);
|
|
1760
|
-
font-size: 0.75rem;
|
|
1761
|
-
}
|
|
1762
|
-
:global(.tools-item > svg:last-child:not(:first-child)) {
|
|
1763
|
-
margin-left: auto;
|
|
1764
|
-
}
|
|
1765
|
-
:global(.tools-footer) {
|
|
1766
|
-
margin-top: 0.25rem;
|
|
1767
|
-
border-top: 1px solid var(--color-border);
|
|
1768
|
-
border-radius: 0 0 calc(var(--kit-radius, var(--radius)) - 2px) calc(var(--kit-radius, var(--radius)) - 2px);
|
|
1769
|
-
color: var(--color-muted-foreground);
|
|
1770
|
-
}
|
|
1771
|
-
:global(.tools-title) {
|
|
1772
|
-
margin: 0;
|
|
1773
|
-
padding: 0.375rem 0.5rem;
|
|
1774
|
-
font-size: 0.875rem;
|
|
1775
|
-
font-weight: 500;
|
|
1776
|
-
}
|
|
1777
|
-
:global(.tools-order) {
|
|
1778
|
-
display: flex;
|
|
1779
|
-
flex-direction: column;
|
|
1780
|
-
gap: 0.125rem;
|
|
1781
|
-
margin: 0;
|
|
1782
|
-
padding: 0;
|
|
1783
|
-
list-style: none;
|
|
1784
|
-
}
|
|
1785
|
-
:global(.tools-row) {
|
|
1786
|
-
display: flex;
|
|
1787
|
-
align-items: center;
|
|
1788
|
-
gap: 0.5rem;
|
|
1789
|
-
min-height: 2rem;
|
|
1790
|
-
padding: 0 0.375rem;
|
|
1791
|
-
border-radius: calc(var(--kit-radius, var(--radius)) - 2px);
|
|
1792
|
-
font-size: 0.875rem;
|
|
1793
|
-
}
|
|
1794
|
-
:global(.tools-row:hover) {
|
|
1795
|
-
background: var(--color-accent);
|
|
1796
|
-
}
|
|
1797
|
-
:global(.tools-row > svg) {
|
|
1798
|
-
width: 1rem;
|
|
1799
|
-
height: 1rem;
|
|
1800
|
-
flex-shrink: 0;
|
|
1801
|
-
color: var(--color-muted-foreground);
|
|
1802
|
-
}
|
|
1803
|
-
:global(.tools-row > span) {
|
|
1804
|
-
flex: 1;
|
|
1805
|
-
min-width: 0;
|
|
1806
|
-
overflow: hidden;
|
|
1807
|
-
text-overflow: ellipsis;
|
|
1808
|
-
white-space: nowrap;
|
|
1809
|
-
}
|
|
1810
|
-
:global(.tools-grip) {
|
|
1811
|
-
display: inline-flex;
|
|
1812
|
-
padding: 0.125rem;
|
|
1813
|
-
border: 0;
|
|
1814
|
-
border-radius: 0.25rem;
|
|
1815
|
-
background: none;
|
|
1816
|
-
color: var(--color-muted-foreground);
|
|
1817
|
-
cursor: grab;
|
|
1818
|
-
touch-action: none;
|
|
1819
|
-
}
|
|
1820
|
-
:global(.tools-grip:focus-visible) {
|
|
1821
|
-
outline: 2px solid var(--color-ring);
|
|
1822
|
-
}
|
|
1823
|
-
:global(.tools-grip svg) {
|
|
1824
|
-
width: 0.875rem;
|
|
1825
|
-
height: 0.875rem;
|
|
1826
|
-
}
|
|
1827
|
-
.plan-badge {
|
|
1828
|
-
padding: 0.125rem 0.5rem;
|
|
1829
|
-
border: 1px solid var(--color-border);
|
|
1830
|
-
border-radius: 999px;
|
|
1831
|
-
color: var(--color-muted-foreground);
|
|
1832
|
-
font-size: var(--kit-font-size-sm, 0.8125rem);
|
|
1833
|
-
white-space: nowrap;
|
|
1834
|
-
}
|
|
1835
|
-
:global(.mode-menu) {
|
|
1836
|
-
width: min(35rem, calc(100vw - 1rem));
|
|
1837
|
-
padding: 0.375rem;
|
|
1838
|
-
border-radius: 0.75rem;
|
|
1839
|
-
}
|
|
1840
|
-
:global(.mode-option) {
|
|
1841
|
-
display: grid;
|
|
1842
|
-
grid-template-columns: 1rem 7.5rem minmax(0, 1fr) 1rem 0.875rem;
|
|
1843
|
-
align-items: center;
|
|
1844
|
-
column-gap: 0.5rem;
|
|
1845
|
-
min-height: 2.125rem;
|
|
1846
|
-
padding: 0.375rem 0.5rem;
|
|
1847
|
-
border-radius: 0.5rem;
|
|
1848
|
-
font-size: var(--kit-font-size-sm, 0.8125rem);
|
|
1849
|
-
}
|
|
1850
|
-
:global(.mode-option[aria-current='true']) {
|
|
1851
|
-
background: var(--color-muted);
|
|
1852
|
-
}
|
|
1853
|
-
:global(.mode-option > svg) {
|
|
1854
|
-
color: var(--color-muted-foreground);
|
|
1855
|
-
width: 0.875rem;
|
|
1856
|
-
height: 0.875rem;
|
|
1857
|
-
}
|
|
1858
|
-
.mode-name {
|
|
1859
|
-
white-space: nowrap;
|
|
1860
|
-
}
|
|
1861
|
-
.mode-description {
|
|
1862
|
-
color: var(--color-muted-foreground);
|
|
1863
|
-
font-size: var(--kit-font-size-xs, 0.75rem);
|
|
1864
|
-
text-align: right;
|
|
1865
|
-
line-height: 1.5;
|
|
1866
|
-
}
|
|
1867
|
-
.mode-notice,
|
|
1868
|
-
.mode-check {
|
|
1869
|
-
display: flex;
|
|
1870
|
-
align-items: center;
|
|
1871
|
-
justify-content: center;
|
|
1872
|
-
color: var(--color-muted-foreground);
|
|
1873
|
-
}
|
|
1874
|
-
.mode-notice {
|
|
1875
|
-
color: var(--color-warning, var(--color-muted-foreground));
|
|
1876
|
-
}
|
|
1877
|
-
.mode-notice :global(svg),
|
|
1878
|
-
.mode-check :global(svg) {
|
|
1879
|
-
display: block;
|
|
1880
|
-
width: 0.8125rem;
|
|
1881
|
-
height: 0.8125rem;
|
|
1882
|
-
}
|
|
1883
|
-
:global(.mode-danger),
|
|
1884
|
-
:global(.mode-danger) :is(.mode-name, .mode-description, .mode-notice),
|
|
1885
|
-
:global(.mode-option.mode-danger > svg) {
|
|
1886
|
-
color: var(--color-destructive);
|
|
1887
|
-
}
|
|
1888
|
-
.mode-footer {
|
|
1889
|
-
display: flex;
|
|
1890
|
-
justify-content: space-between;
|
|
1891
|
-
align-items: center;
|
|
1892
|
-
gap: 0.75rem;
|
|
1893
|
-
padding: 0.625rem 0.375rem 0.125rem;
|
|
1894
|
-
color: var(--color-muted-foreground);
|
|
1895
|
-
font-size: var(--kit-font-size-xs, 0.75rem);
|
|
1896
|
-
}
|
|
1897
|
-
.mode-keys {
|
|
1898
|
-
display: flex;
|
|
1899
|
-
align-items: center;
|
|
1900
|
-
gap: 0.25rem;
|
|
1901
|
-
}
|
|
1902
|
-
.mode-keys kbd {
|
|
1903
|
-
padding: 0 0.25rem;
|
|
1904
|
-
border-radius: 0.25rem;
|
|
1905
|
-
background: var(--color-muted);
|
|
1906
|
-
font: inherit;
|
|
1907
|
-
}
|
|
1908
|
-
.mode-keys kbd:not(:first-child) {
|
|
1909
|
-
margin-left: 0.375rem;
|
|
1910
|
-
}
|
|
1911
|
-
@media (max-width: 480px) {
|
|
1912
|
-
:global(.mode-option) {
|
|
1913
|
-
grid-template-columns: 1rem minmax(0, 1fr) 1rem 0.875rem;
|
|
1914
|
-
row-gap: 0.125rem;
|
|
1915
|
-
}
|
|
1916
|
-
.mode-description {
|
|
1917
|
-
grid-column: 2;
|
|
1918
|
-
grid-row: 2;
|
|
1919
|
-
text-align: left;
|
|
1920
|
-
}
|
|
1921
|
-
.mode-notice {
|
|
1922
|
-
grid-column: 3;
|
|
1923
|
-
grid-row: 1;
|
|
1924
|
-
}
|
|
1925
|
-
.mode-check {
|
|
1926
|
-
grid-column: 4;
|
|
1927
|
-
grid-row: 1;
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
:global(.reasoning-popover) {
|
|
1931
|
-
box-sizing: border-box;
|
|
1932
|
-
width: min(18rem, calc(100vw - 1rem));
|
|
1933
|
-
max-height: calc(100dvh - 1rem);
|
|
1934
|
-
overflow-y: auto;
|
|
1935
|
-
display: flex;
|
|
1936
|
-
flex-direction: column;
|
|
1937
|
-
gap: 0.75rem;
|
|
1938
|
-
padding: 0.75rem;
|
|
1939
|
-
border: 1px solid var(--color-border);
|
|
1940
|
-
border-radius: var(--kit-radius, var(--radius));
|
|
1941
|
-
background: var(--color-popover);
|
|
1942
|
-
color: var(--color-popover-foreground);
|
|
1943
|
-
box-shadow: 0 4px 12px color-mix(in oklch, var(--color-foreground) 12%, transparent);
|
|
1944
|
-
font-size: var(--kit-font-size-sm, 0.8125rem);
|
|
1945
|
-
}
|
|
1946
|
-
.reasoning-heading,
|
|
1947
|
-
.reasoning-scale {
|
|
1948
|
-
display: flex;
|
|
1949
|
-
justify-content: space-between;
|
|
1950
|
-
align-items: center;
|
|
1951
|
-
gap: 0.5rem;
|
|
1952
|
-
}
|
|
1953
|
-
.reasoning-heading p,
|
|
1954
|
-
.reasoning-hint {
|
|
1955
|
-
margin: 0;
|
|
1956
|
-
overflow-wrap: anywhere;
|
|
1957
|
-
}
|
|
1958
|
-
.reasoning-scale,
|
|
1959
|
-
.reasoning-hint {
|
|
1960
|
-
color: var(--color-muted-foreground);
|
|
1961
|
-
}
|
|
1962
|
-
:global(.reasoning-popover input) {
|
|
1963
|
-
accent-color: var(--color-primary);
|
|
1964
|
-
}
|
|
1965
|
-
.reasoning-slider {
|
|
1966
|
-
position: relative;
|
|
1967
|
-
height: 1.75rem;
|
|
1968
|
-
display: flex;
|
|
1969
|
-
align-items: center;
|
|
1970
|
-
}
|
|
1971
|
-
:global(.reasoning-slider input[type='range']) {
|
|
1972
|
-
appearance: none;
|
|
1973
|
-
width: 100%;
|
|
1974
|
-
height: 1.75rem;
|
|
1975
|
-
margin: 0;
|
|
1976
|
-
background: transparent;
|
|
1977
|
-
cursor: pointer;
|
|
1978
|
-
}
|
|
1979
|
-
:global(.reasoning-slider input::-webkit-slider-runnable-track) {
|
|
1980
|
-
height: 0.375rem;
|
|
1981
|
-
border-radius: 999px;
|
|
1982
|
-
background: linear-gradient(
|
|
1983
|
-
to right,
|
|
1984
|
-
var(--color-primary) var(--reasoning-progress),
|
|
1985
|
-
var(--color-muted) var(--reasoning-progress)
|
|
1986
|
-
);
|
|
1987
|
-
}
|
|
1988
|
-
:global(.reasoning-slider input::-moz-range-track) {
|
|
1989
|
-
height: 0.375rem;
|
|
1990
|
-
border-radius: 999px;
|
|
1991
|
-
background: linear-gradient(
|
|
1992
|
-
to right,
|
|
1993
|
-
var(--color-primary) var(--reasoning-progress),
|
|
1994
|
-
var(--color-muted) var(--reasoning-progress)
|
|
1995
|
-
);
|
|
1996
|
-
}
|
|
1997
|
-
:global(.reasoning-slider input::-webkit-slider-thumb) {
|
|
1998
|
-
appearance: none;
|
|
1999
|
-
width: 1.125rem;
|
|
2000
|
-
height: 1.125rem;
|
|
2001
|
-
margin-top: -0.375rem;
|
|
2002
|
-
border: 1px solid var(--color-border);
|
|
2003
|
-
border-radius: 50%;
|
|
2004
|
-
background: var(--color-popover);
|
|
2005
|
-
}
|
|
2006
|
-
:global(.reasoning-slider input::-moz-range-thumb) {
|
|
2007
|
-
width: 1.125rem;
|
|
2008
|
-
height: 1.125rem;
|
|
2009
|
-
box-sizing: border-box;
|
|
2010
|
-
border: 1px solid var(--color-border);
|
|
2011
|
-
border-radius: 50%;
|
|
2012
|
-
background: var(--color-popover);
|
|
2013
|
-
}
|
|
2014
|
-
.reasoning-ticks {
|
|
2015
|
-
position: absolute;
|
|
2016
|
-
inset: 50% 0.5625rem auto;
|
|
2017
|
-
pointer-events: none;
|
|
2018
|
-
}
|
|
2019
|
-
.reasoning-tick {
|
|
2020
|
-
position: absolute;
|
|
2021
|
-
width: 0.25rem;
|
|
2022
|
-
height: 0.25rem;
|
|
2023
|
-
border-radius: 50%;
|
|
2024
|
-
background: var(--color-popover);
|
|
2025
|
-
transform: translate(-50%, -50%);
|
|
2026
|
-
}
|
|
2027
|
-
.reasoning-tick.current {
|
|
2028
|
-
visibility: hidden;
|
|
2029
|
-
}
|
|
2030
|
-
:global(.reasoning-slider input:disabled) {
|
|
2031
|
-
cursor: not-allowed;
|
|
2032
|
-
opacity: 0.5;
|
|
2033
|
-
}
|
|
2034
|
-
:global(.reasoning-popover input:focus-visible) {
|
|
2035
|
-
outline: 2px solid var(--color-ring);
|
|
2036
|
-
outline-offset: 2px;
|
|
2037
|
-
}
|
|
2038
|
-
.reasoning-option {
|
|
2039
|
-
display: flex;
|
|
2040
|
-
align-items: center;
|
|
2041
|
-
gap: 0.5rem;
|
|
2042
|
-
}
|
|
2043
|
-
.skills-control {
|
|
2044
|
-
min-width: 0;
|
|
2045
|
-
max-width: 100%;
|
|
2046
|
-
}
|
|
2047
|
-
.skills-control :global(div:first-child) {
|
|
2048
|
-
flex-wrap: wrap;
|
|
2049
|
-
min-width: 0;
|
|
2050
|
-
}
|
|
2051
|
-
.skills-control :global([data-slot='select-trigger']) {
|
|
2052
|
-
width: auto;
|
|
2053
|
-
max-width: 100%;
|
|
2054
|
-
}
|
|
2055
|
-
.search-status,
|
|
2056
|
-
.draft-recovery {
|
|
2057
|
-
margin: 0;
|
|
2058
|
-
color: var(--color-muted-foreground);
|
|
2059
|
-
font-size: var(--kit-font-size-sm, 0.8125rem);
|
|
2060
|
-
}
|
|
2061
|
-
.draft-recovery {
|
|
2062
|
-
display: flex;
|
|
2063
|
-
flex-wrap: wrap;
|
|
2064
|
-
align-items: center;
|
|
2065
|
-
gap: 0.5rem;
|
|
2066
|
-
}
|
|
2067
|
-
.history-list {
|
|
2068
|
-
display: flex;
|
|
2069
|
-
flex-direction: column;
|
|
2070
|
-
gap: 0.25rem;
|
|
2071
|
-
max-height: 40vh;
|
|
2072
|
-
overflow-y: auto;
|
|
2073
|
-
min-width: 0;
|
|
2074
|
-
}
|
|
2075
|
-
.history-list :global(button) {
|
|
2076
|
-
justify-content: flex-start;
|
|
2077
|
-
min-width: 0;
|
|
2078
|
-
}
|
|
2079
|
-
.history-list span {
|
|
2080
|
-
min-width: 0;
|
|
2081
|
-
overflow: hidden;
|
|
2082
|
-
text-overflow: ellipsis;
|
|
2083
|
-
white-space: nowrap;
|
|
2084
|
-
}
|
|
2085
|
-
:global(.reply-dialog) {
|
|
2086
|
-
max-height: calc(100dvh - 2rem);
|
|
2087
|
-
overflow-y: auto;
|
|
2088
|
-
}
|
|
2089
|
-
:global(.reply-dialog form),
|
|
2090
|
-
:global(.reply-dialog [data-slot='field-group']) {
|
|
2091
|
-
min-width: 0;
|
|
2092
|
-
}
|
|
2093
|
-
:global(.reply-dialog textarea) {
|
|
2094
|
-
max-height: 45dvh;
|
|
2095
|
-
overflow-y: auto;
|
|
2096
|
-
}
|
|
2097
|
-
</style>
|