@veltra/ai 2.1.6 → 2.1.8
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/README.md +42 -0
- package/dist/chat/fold.d.ts +41 -0
- package/dist/chat/fold.js +208 -0
- package/dist/chat/fold.js.map +1 -0
- package/dist/chat/index.d.ts +4 -2
- package/dist/chat/session.d.ts +123 -0
- package/dist/chat/session.js +79 -0
- package/dist/chat/session.js.map +1 -0
- package/dist/chat/types.d.ts +38 -1
- package/dist/chat/use-chat-helpers.js +39 -0
- package/dist/chat/use-chat-helpers.js.map +1 -0
- package/dist/chat/use-chat-session.js +68 -0
- package/dist/chat/use-chat-session.js.map +1 -0
- package/dist/chat/use-chat.d.ts +50 -2
- package/dist/chat/use-chat.js +73 -39
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +49 -14
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/approval-banner.js +54 -0
- package/dist/components/ai-chat/approval-banner.js.map +1 -0
- package/dist/components/ai-chat/ask-question.js +32 -51
- package/dist/components/ai-chat/ask-question.js.map +1 -1
- package/dist/components/ai-chat/di.js.map +1 -1
- package/dist/components/ai-chat/job-bar.js +48 -0
- package/dist/components/ai-chat/job-bar.js.map +1 -0
- package/dist/components/ai-chat/message-list.js +5 -2
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/queue-list.js +9 -3
- package/dist/components/ai-chat/queue-list.js.map +1 -1
- package/dist/components/ai-chat/side-panel.js +12 -30
- package/dist/components/ai-chat/side-panel.js.map +1 -1
- package/dist/components/ai-chat/style2.css +146 -8
- package/dist/components/ai-chat/tool-call.js +51 -40
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-chat/tool-icons.js +31 -0
- package/dist/components/ai-chat/tool-icons.js.map +1 -0
- package/dist/components/ai-chat/tool-json.js +40 -0
- package/dist/components/ai-chat/tool-json.js.map +1 -0
- package/dist/components/ai-orb/style2.css +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -1
- package/dist/tools/ask-question/ask-question.js +48 -2
- package/dist/tools/ask-question/ask-question.js.map +1 -1
- package/dist/types/ai-chat.d.ts +17 -6
- package/package.json +7 -7
- package/src/chat/__test__/fold.test.ts +136 -0
- package/src/chat/__test__/session.test.ts +191 -0
- package/src/chat/__test__/use-chat.test.ts +268 -1
- package/src/chat/fold.ts +260 -0
- package/src/chat/index.ts +2 -0
- package/src/chat/session.ts +168 -0
- package/src/chat/types.ts +40 -0
- package/src/chat/use-chat-helpers.ts +39 -0
- package/src/chat/use-chat-session.ts +87 -0
- package/src/chat/use-chat.ts +90 -44
- package/src/components/ai-chat/__test__/ai-chat.test.ts +405 -7
- package/src/components/ai-chat/__test__/tool-icons.test.ts +33 -0
- package/src/components/ai-chat/ai-chat.vue +39 -4
- package/src/components/ai-chat/approval-banner.vue +37 -0
- package/src/components/ai-chat/ask-question.vue +38 -54
- package/src/components/ai-chat/di.ts +6 -4
- package/src/components/ai-chat/job-bar.vue +41 -0
- package/src/components/ai-chat/message-list.vue +4 -1
- package/src/components/ai-chat/queue-list.vue +3 -1
- package/src/components/ai-chat/side-panel.vue +15 -13
- package/src/components/ai-chat/style.scss +158 -6
- package/src/components/ai-chat/tool-call.vue +46 -34
- package/src/components/ai-chat/tool-icons.ts +33 -0
- package/src/components/ai-chat/tool-json.vue +52 -0
- package/src/tools/ask-question/ask-question.ts +53 -4
- package/src/types/ai-chat.ts +17 -5
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
</template>
|
|
82
82
|
|
|
83
83
|
<!-- 提交后:问答摘要 -->
|
|
84
|
-
<template v-else-if="
|
|
84
|
+
<template v-else-if="answers != null">
|
|
85
85
|
<div v-for="(item, i) in doneAnswers" :key="i" :class="cls.e('ask-question-result')">
|
|
86
86
|
<div :class="cls.e('ask-question-result-q')">
|
|
87
87
|
<span
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
</template>
|
|
102
102
|
|
|
103
103
|
<!-- 异常 / 取消 -->
|
|
104
|
-
<div v-else :class="cls.e('ask-question-status')">{{
|
|
104
|
+
<div v-else :class="cls.e('ask-question-status')">{{ error }}</div>
|
|
105
105
|
</div>
|
|
106
106
|
</template>
|
|
107
107
|
|
|
@@ -110,39 +110,34 @@ import { UButton, UInput } from '@veltra/desktop'
|
|
|
110
110
|
import { bem } from '@veltra/utils'
|
|
111
111
|
import { computed, inject, ref, watch } from 'vue'
|
|
112
112
|
|
|
113
|
-
import type { ChatToolCall } from '../../chat/types'
|
|
114
113
|
import type {
|
|
115
|
-
|
|
114
|
+
AskQuestionAnswer,
|
|
116
115
|
AskQuestionItem,
|
|
117
116
|
AskQuestionResult
|
|
118
117
|
} from '../../tools/ask-question/ask-question'
|
|
119
|
-
import { resolveAskQuestion } from '../../tools/ask-question/deferred'
|
|
120
118
|
import { AiChatDIKey } from './di'
|
|
121
119
|
|
|
122
120
|
defineOptions({ name: 'UAiChatAskQuestion' })
|
|
123
121
|
|
|
124
|
-
const props = defineProps<{
|
|
122
|
+
const props = defineProps<{
|
|
123
|
+
/** 问题列表;组件不再从 toolCall 解析参数 */
|
|
124
|
+
questions: AskQuestionItem[]
|
|
125
|
+
/** 已提交摘要;有值则展示结果而非表单 */
|
|
126
|
+
answers?: AskQuestionAnswer[]
|
|
127
|
+
/** 失败 / 取消文案 */
|
|
128
|
+
error?: string
|
|
129
|
+
}>()
|
|
130
|
+
|
|
131
|
+
const emit = defineEmits<{
|
|
132
|
+
/** 提交问答;值为答案数组(与 AskQuestionResult 兼容) */
|
|
133
|
+
(e: 'submit', value: AskQuestionAnswer[] | AskQuestionResult): void
|
|
134
|
+
}>()
|
|
125
135
|
|
|
126
136
|
const di = inject(AiChatDIKey)
|
|
127
137
|
const cls = di?.cls ?? bem('ai-chat')
|
|
128
138
|
|
|
129
|
-
/**
|
|
130
|
-
const
|
|
131
|
-
try {
|
|
132
|
-
const args = JSON.parse(props.toolCall.arguments || '{}') as Partial<AskQuestionArgs>
|
|
133
|
-
return Array.isArray(args.questions) ? args.questions : []
|
|
134
|
-
} catch {
|
|
135
|
-
return []
|
|
136
|
-
}
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
/** 作答中:execute 挂起等待用户提交 */
|
|
140
|
-
const answering = computed(() => {
|
|
141
|
-
return (
|
|
142
|
-
props.toolCall.result == null &&
|
|
143
|
-
(props.toolCall.status === 'running' || props.toolCall.status === 'pending')
|
|
144
|
-
)
|
|
145
|
-
})
|
|
139
|
+
/** 作答中:尚未提交、也没有失败文案 */
|
|
140
|
+
const answering = computed(() => props.answers == null && !props.error)
|
|
146
141
|
|
|
147
142
|
/** 当前题索引 */
|
|
148
143
|
const current = ref(0)
|
|
@@ -152,16 +147,16 @@ const selections = ref<(string | null)[]>([])
|
|
|
152
147
|
const customs = ref<string[]>([])
|
|
153
148
|
|
|
154
149
|
watch(
|
|
155
|
-
questions,
|
|
150
|
+
() => props.questions,
|
|
156
151
|
(list) => {
|
|
157
|
-
if (props.
|
|
152
|
+
if (props.answers != null) return
|
|
158
153
|
selections.value = list.map(() => null)
|
|
159
154
|
customs.value = list.map(() => '')
|
|
160
155
|
},
|
|
161
156
|
{ immediate: true }
|
|
162
157
|
)
|
|
163
158
|
|
|
164
|
-
const currentQuestion = computed(() => questions
|
|
159
|
+
const currentQuestion = computed(() => props.questions[current.value])
|
|
165
160
|
|
|
166
161
|
/** 某题是否已作答:自定义输入非空优先,否则看选中项 */
|
|
167
162
|
const isAnswered = (index: number) => {
|
|
@@ -169,11 +164,11 @@ const isAnswered = (index: number) => {
|
|
|
169
164
|
}
|
|
170
165
|
|
|
171
166
|
const allAnswered = computed(() => {
|
|
172
|
-
return questions.
|
|
167
|
+
return props.questions.length > 0 && props.questions.every((_, i) => isAnswered(i))
|
|
173
168
|
})
|
|
174
169
|
|
|
175
170
|
const goTo = (index: number) => {
|
|
176
|
-
if (index < 0 || index >= questions.
|
|
171
|
+
if (index < 0 || index >= props.questions.length) return
|
|
177
172
|
current.value = index
|
|
178
173
|
}
|
|
179
174
|
|
|
@@ -192,41 +187,30 @@ const onCustomInput = (value: string) => {
|
|
|
192
187
|
}
|
|
193
188
|
|
|
194
189
|
const handleSubmit = () => {
|
|
195
|
-
const firstUnanswered = questions.
|
|
190
|
+
const firstUnanswered = props.questions.findIndex((_, i) => !isAnswered(i))
|
|
196
191
|
if (firstUnanswered >= 0) {
|
|
197
192
|
current.value = firstUnanswered
|
|
198
193
|
return
|
|
199
194
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
195
|
+
emit(
|
|
196
|
+
'submit',
|
|
197
|
+
props.questions.map((item, i) => ({
|
|
198
|
+
question: item.question,
|
|
199
|
+
answer: customs.value[i]?.trim() || selections.value[i] || ''
|
|
200
|
+
}))
|
|
201
|
+
)
|
|
205
202
|
}
|
|
206
203
|
|
|
207
|
-
/**
|
|
204
|
+
/** 提交后的问答摘要:命中预设选项的回答渲染为 chip */
|
|
208
205
|
const doneAnswers = computed(() => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
if (!Array.isArray(result?.answers)) return []
|
|
217
|
-
return result.answers.map((item) => ({
|
|
218
|
-
...item,
|
|
219
|
-
/** 命中预设选项的回答渲染为 chip */
|
|
220
|
-
isOption: questions.value.some(
|
|
206
|
+
const list = props.answers
|
|
207
|
+
if (!list?.length) return []
|
|
208
|
+
return list.map((item) => ({
|
|
209
|
+
question: item.question,
|
|
210
|
+
answer: item.answer,
|
|
211
|
+
isOption: props.questions.some(
|
|
221
212
|
(q) => q.question === item.question && q.options?.includes(item.answer)
|
|
222
213
|
)
|
|
223
214
|
}))
|
|
224
215
|
})
|
|
225
|
-
|
|
226
|
-
const statusText = computed(() => {
|
|
227
|
-
if (props.toolCall.status !== 'error') return ''
|
|
228
|
-
const err = props.toolCall.error ?? ''
|
|
229
|
-
if (err.includes('Abort') || err.includes('取消')) return '已取消'
|
|
230
|
-
return err || '提问失败'
|
|
231
|
-
})
|
|
232
216
|
</script>
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type { BEM } from '@veltra/utils'
|
|
2
|
-
import type { ComputedRef, InjectionKey, Slots } from 'vue'
|
|
2
|
+
import type { Component, ComputedRef, InjectionKey, Slots } from 'vue'
|
|
3
3
|
|
|
4
|
-
import type { ChatTool } from '../../chat/types'
|
|
4
|
+
import type { ChatTool, ChatToolMeta } from '../../chat/types'
|
|
5
5
|
|
|
6
6
|
export interface AiChatContext {
|
|
7
7
|
/** ai-chat 组件的 BEM 类生成器 */
|
|
8
8
|
cls: BEM<'ai-chat'>
|
|
9
9
|
/** ai-chat 使用方提供的插槽(用于 tool-<name> 动态插槽) */
|
|
10
10
|
slots: Slots
|
|
11
|
-
/** 按 name
|
|
12
|
-
tools: ComputedRef<Record<string, ChatTool | undefined>>
|
|
11
|
+
/** 按 name 索引的工具定义或渲染元信息(解析 icon/label/render/autoCollapse) */
|
|
12
|
+
tools: ComputedRef<Record<string, ChatTool | ChatToolMeta | undefined>>
|
|
13
|
+
/** 宿主覆盖的 toolName → icon;缺省走包内名称规则 */
|
|
14
|
+
toolIcons: ComputedRef<Record<string, Component> | undefined>
|
|
13
15
|
/** 打开右侧面板并聚焦到指定工具调用(renderTo: 'panel' 工具的「查看面板」入口) */
|
|
14
16
|
openPanel: (toolCallId: string) => void
|
|
15
17
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="jobs.length" :class="cls.e('job-bar')">
|
|
3
|
+
<button type="button" :class="cls.e('job-bar-head')" @click="expanded = !expanded">
|
|
4
|
+
<span>作业 · {{ jobs.length }}</span>
|
|
5
|
+
<UIcon :class="[cls.e('job-bar-chevron'), bem.is('expanded', expanded)]">
|
|
6
|
+
<ArrowRight />
|
|
7
|
+
</UIcon>
|
|
8
|
+
</button>
|
|
9
|
+
<div v-if="expanded" :class="cls.e('job-bar-list')">
|
|
10
|
+
<div v-for="job in jobs" :key="job.id" :class="cls.e('job-bar-item')">
|
|
11
|
+
<UIcon :class="cls.e('job-bar-icon')">
|
|
12
|
+
<component :is="resolveToolIcon(job.kind)" />
|
|
13
|
+
</UIcon>
|
|
14
|
+
<span :class="[cls.e('job-bar-label'), job.status === 'running' && 'u-shine']">
|
|
15
|
+
{{ job.label }}
|
|
16
|
+
</span>
|
|
17
|
+
<span :class="[cls.e('job-bar-dot'), bem.is(job.status)]" />
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script lang="ts" setup>
|
|
24
|
+
import { UIcon } from '@veltra/desktop'
|
|
25
|
+
import { ArrowRight } from '@veltra/icons/normal'
|
|
26
|
+
import { bem } from '@veltra/utils'
|
|
27
|
+
import { inject, ref } from 'vue'
|
|
28
|
+
|
|
29
|
+
import type { ChatJob } from '../../chat/types'
|
|
30
|
+
import { AiChatDIKey } from './di'
|
|
31
|
+
import { resolveToolIcon } from './tool-icons'
|
|
32
|
+
|
|
33
|
+
defineOptions({ name: 'UAiChatJobBar' })
|
|
34
|
+
|
|
35
|
+
defineProps<{ jobs: ChatJob[] }>()
|
|
36
|
+
|
|
37
|
+
const expanded = ref(true)
|
|
38
|
+
|
|
39
|
+
const di = inject(AiChatDIKey)
|
|
40
|
+
const cls = di?.cls ?? bem('ai-chat')
|
|
41
|
+
</script>
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
:key="welcomeIndex"
|
|
71
71
|
type="button"
|
|
72
72
|
:class="cls.e('welcome-item')"
|
|
73
|
-
|
|
73
|
+
:disabled="readonly"
|
|
74
|
+
@click="!readonly && emit('welcome-click', currentWelcomeItem)"
|
|
74
75
|
>
|
|
75
76
|
{{ currentWelcomeItem }}
|
|
76
77
|
</button>
|
|
@@ -128,6 +129,8 @@ const props = defineProps<{
|
|
|
128
129
|
welcome?: string | string[]
|
|
129
130
|
/** 是否生成中(控制底部工作状态指示) */
|
|
130
131
|
running?: boolean
|
|
132
|
+
/** 只读时欢迎语不可点击发送 */
|
|
133
|
+
readonly?: boolean
|
|
131
134
|
/** 透传给 MarkdownRender 的属性 */
|
|
132
135
|
rendererProps?: Record<string, unknown>
|
|
133
136
|
}>()
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<div :class="cls.e('queue-head')">待发送队列 · {{ queue.length }}</div>
|
|
4
4
|
<div v-for="item in queue" :key="item.id" :class="cls.e('queue-item')">
|
|
5
5
|
<span :class="cls.e('queue-text')" :title="item.content">{{ item.content }}</span>
|
|
6
|
-
<span :class="cls.e('queue-actions')">
|
|
6
|
+
<span v-if="!readonly" :class="cls.e('queue-actions')">
|
|
7
7
|
<button
|
|
8
8
|
type="button"
|
|
9
9
|
:class="[cls.e('queue-action'), cls.em('queue-action', 'primary')]"
|
|
@@ -47,6 +47,8 @@ defineOptions({ name: 'UAiChatQueueList' })
|
|
|
47
47
|
defineProps<{
|
|
48
48
|
/** 待发送队列(按执行顺序排列) */
|
|
49
49
|
queue: ChatQueuedMessage[]
|
|
50
|
+
/** 只读时不展示插队 / 编辑 / 移除 */
|
|
51
|
+
readonly?: boolean
|
|
50
52
|
}>()
|
|
51
53
|
|
|
52
54
|
const emit = defineEmits<{
|
|
@@ -22,12 +22,13 @@
|
|
|
22
22
|
|
|
23
23
|
<script lang="ts" setup>
|
|
24
24
|
import { UIcon, UScroll } from '@veltra/desktop'
|
|
25
|
-
import {
|
|
25
|
+
import { Close } from '@veltra/icons/normal'
|
|
26
26
|
import { bem } from '@veltra/utils'
|
|
27
|
-
import { computed, inject
|
|
27
|
+
import { computed, inject } from 'vue'
|
|
28
28
|
|
|
29
29
|
import type { ChatToolCall } from '../../chat/types'
|
|
30
30
|
import { AiChatDIKey } from './di'
|
|
31
|
+
import { resolveToolIcon } from './tool-icons'
|
|
31
32
|
|
|
32
33
|
defineOptions({ name: 'UAiChatSidePanel' })
|
|
33
34
|
|
|
@@ -41,20 +42,21 @@ const cls = di?.cls ?? bem('ai-chat')
|
|
|
41
42
|
/** 当前调用对应的工具定义(解析 icon/label/render) */
|
|
42
43
|
const tool = computed(() => di?.tools.value[props.toolCall.name])
|
|
43
44
|
|
|
44
|
-
const
|
|
45
|
-
pending:
|
|
46
|
-
running:
|
|
47
|
-
'awaiting-confirm':
|
|
48
|
-
success:
|
|
49
|
-
error:
|
|
50
|
-
rejected:
|
|
45
|
+
const STATUS_CLASS: Record<string, string> = {
|
|
46
|
+
pending: 'is-active',
|
|
47
|
+
running: 'is-active',
|
|
48
|
+
'awaiting-confirm': 'is-warning',
|
|
49
|
+
success: 'is-success',
|
|
50
|
+
error: 'is-danger',
|
|
51
|
+
rejected: 'is-danger'
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
const
|
|
54
|
-
const statusClass = computed(() => STATUS_META[props.toolCall.status]?.class ?? '')
|
|
54
|
+
const statusClass = computed(() => STATUS_CLASS[props.toolCall.status] ?? '')
|
|
55
55
|
|
|
56
|
-
/**
|
|
57
|
-
const headerIcon = computed(
|
|
56
|
+
/** 头部图标:meta.icon > 宿主覆盖 > 内置名称规则 > 兜底;状态点走 status class */
|
|
57
|
+
const headerIcon = computed(
|
|
58
|
+
() => tool.value?.icon ?? resolveToolIcon(props.toolCall.name, di?.toolIcons?.value)
|
|
59
|
+
)
|
|
58
60
|
|
|
59
61
|
/** 面板标题:优先工具定义的 panelTitle(业务对象 + 动作),缺省回落到 label ?? name */
|
|
60
62
|
const panelTitle = computed(() => {
|
|
@@ -161,7 +161,7 @@ $root-name: ai-chat;
|
|
|
161
161
|
@include m.e(panel-fallback) {
|
|
162
162
|
margin: 0;
|
|
163
163
|
padding: fn.use-var(gap, default);
|
|
164
|
-
font-family:
|
|
164
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
165
165
|
font-size: fn.use-var(font-size-main, small);
|
|
166
166
|
line-height: 1.5;
|
|
167
167
|
white-space: pre-wrap;
|
|
@@ -265,6 +265,16 @@ $root-name: ai-chat;
|
|
|
265
265
|
color: fn.use-var(color, primary);
|
|
266
266
|
background-color: fn.use-var(color, primary, 'light-9');
|
|
267
267
|
}
|
|
268
|
+
|
|
269
|
+
&:disabled {
|
|
270
|
+
cursor: default;
|
|
271
|
+
color: fn.use-var(text-color, second);
|
|
272
|
+
|
|
273
|
+
&:hover {
|
|
274
|
+
color: fn.use-var(text-color, second);
|
|
275
|
+
background-color: fn.use-var(bg-color, top);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
268
278
|
}
|
|
269
279
|
|
|
270
280
|
// 欢迎语逐条轮换的切换过渡
|
|
@@ -621,19 +631,31 @@ $root-name: ai-chat;
|
|
|
621
631
|
|
|
622
632
|
@include m.e(tool-call-section) {
|
|
623
633
|
color: fn.use-var(text-color, assist);
|
|
624
|
-
|
|
634
|
+
}
|
|
625
635
|
|
|
636
|
+
@include m.e(tool-call-json) {
|
|
626
637
|
&:not(:first-child) {
|
|
627
638
|
margin-top: fn.use-var(gap, small);
|
|
628
639
|
}
|
|
629
640
|
}
|
|
630
641
|
|
|
642
|
+
@include m.e(tool-call-json-head) {
|
|
643
|
+
@include m.flex(flex, flex-start, center);
|
|
644
|
+
gap: fn.use-var(gap, small);
|
|
645
|
+
margin-bottom: 4px;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
@include m.e(tool-call-json-toggle) {
|
|
649
|
+
flex-shrink: 0;
|
|
650
|
+
margin-left: auto;
|
|
651
|
+
}
|
|
652
|
+
|
|
631
653
|
@include m.e(tool-call-code) {
|
|
632
654
|
margin: 0;
|
|
633
655
|
padding: 4px fn.use-var(gap, small);
|
|
634
656
|
border-radius: fn.use-var(radius, small);
|
|
635
657
|
background-color: fn.use-var(bg-color, bottom);
|
|
636
|
-
font-family:
|
|
658
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
637
659
|
font-size: fn.use-var(font-size-main, small);
|
|
638
660
|
line-height: 1.5;
|
|
639
661
|
white-space: pre-wrap;
|
|
@@ -866,6 +888,136 @@ $root-name: ai-chat;
|
|
|
866
888
|
}
|
|
867
889
|
}
|
|
868
890
|
|
|
891
|
+
// 作业条:有 jobs 时出现在输入区上方
|
|
892
|
+
@include m.e(job-bar) {
|
|
893
|
+
flex-shrink: 0;
|
|
894
|
+
width: 100%;
|
|
895
|
+
max-width: 800px;
|
|
896
|
+
box-sizing: border-box;
|
|
897
|
+
margin-bottom: 4px;
|
|
898
|
+
border-radius: fn.use-var(radius, default);
|
|
899
|
+
background-color: fn.use-var(bg-color, middle);
|
|
900
|
+
padding: 4px;
|
|
901
|
+
font-size: fn.use-var(font-size-main, small);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
@include m.e(job-bar-head) {
|
|
905
|
+
@include m.flex(flex, space-between, center);
|
|
906
|
+
width: 100%;
|
|
907
|
+
border: none;
|
|
908
|
+
background: none;
|
|
909
|
+
padding: 2px 6px;
|
|
910
|
+
color: fn.use-var(text-color, assist);
|
|
911
|
+
font-size: inherit;
|
|
912
|
+
font-family: inherit;
|
|
913
|
+
cursor: pointer;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
@include m.e(job-bar-chevron) {
|
|
917
|
+
font-size: 12px;
|
|
918
|
+
transition: transform 0.2s;
|
|
919
|
+
|
|
920
|
+
@include m.is(expanded) {
|
|
921
|
+
transform: rotate(90deg);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
@include m.e(job-bar-list) {
|
|
926
|
+
display: flex;
|
|
927
|
+
flex-direction: column;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
@include m.e(job-bar-item) {
|
|
931
|
+
@include m.flex(flex, flex-start, center);
|
|
932
|
+
gap: 6px;
|
|
933
|
+
padding: 3px 6px;
|
|
934
|
+
border-radius: fn.use-var(radius, small);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
@include m.e(job-bar-icon) {
|
|
938
|
+
flex-shrink: 0;
|
|
939
|
+
font-size: 13px;
|
|
940
|
+
color: fn.use-var(text-color, second);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
@include m.e(job-bar-label) {
|
|
944
|
+
flex: 1;
|
|
945
|
+
min-width: 0;
|
|
946
|
+
@include m.ellipsis;
|
|
947
|
+
color: fn.use-var(text-color, main);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
@include m.e(job-bar-dot) {
|
|
951
|
+
flex-shrink: 0;
|
|
952
|
+
width: 6px;
|
|
953
|
+
height: 6px;
|
|
954
|
+
border-radius: 50%;
|
|
955
|
+
background-color: fn.use-var(text-color, placeholder);
|
|
956
|
+
|
|
957
|
+
@include m.is(running) {
|
|
958
|
+
background-color: fn.use-var(color, primary);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
@include m.is(done) {
|
|
962
|
+
background-color: fn.use-var(color, success);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
@include m.is(error) {
|
|
966
|
+
background-color: fn.use-var(color, danger);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// session 提问:与输入区同宽卡片
|
|
971
|
+
@include m.e(session-question) {
|
|
972
|
+
flex-shrink: 0;
|
|
973
|
+
width: 100%;
|
|
974
|
+
max-width: 800px;
|
|
975
|
+
box-sizing: border-box;
|
|
976
|
+
margin-bottom: 4px;
|
|
977
|
+
border-radius: fn.use-var(radius, default);
|
|
978
|
+
background-color: fn.use-var(bg-color, top);
|
|
979
|
+
box-shadow: fn.use-var(shadow, sm);
|
|
980
|
+
padding: fn.use-var(gap, small);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// 无 callId 的审批横幅(浮动确认卡)
|
|
984
|
+
@include m.e(approval-banner) {
|
|
985
|
+
flex-shrink: 0;
|
|
986
|
+
width: 100%;
|
|
987
|
+
max-width: 800px;
|
|
988
|
+
box-sizing: border-box;
|
|
989
|
+
margin-bottom: 4px;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
@include m.e(approval-banner-card) {
|
|
993
|
+
padding: fn.use-var(gap, small);
|
|
994
|
+
border-radius: fn.use-var(radius, default);
|
|
995
|
+
background-color: fn.use-var(bg-color, top);
|
|
996
|
+
box-shadow: fn.use-var(shadow, sm);
|
|
997
|
+
|
|
998
|
+
& + & {
|
|
999
|
+
margin-top: 4px;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
@include m.e(approval-banner-title) {
|
|
1004
|
+
color: fn.use-var(text-color, title);
|
|
1005
|
+
font-weight: 500;
|
|
1006
|
+
font-size: fn.use-var(font-size-main, small);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
@include m.e(approval-banner-reason) {
|
|
1010
|
+
margin-top: 4px;
|
|
1011
|
+
color: fn.use-var(text-color, assist);
|
|
1012
|
+
font-size: fn.use-var(font-size-main, small);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
@include m.e(approval-banner-actions) {
|
|
1016
|
+
@include m.flex(flex, flex-end, center);
|
|
1017
|
+
gap: fn.use-var(gap, small);
|
|
1018
|
+
margin-top: fn.use-var(gap, small);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
869
1021
|
// 输入区:无边框卡片,靠浅阴影区分层级;聚焦不换蓝描边,避免变成另一种框
|
|
870
1022
|
@include m.e(input-area) {
|
|
871
1023
|
flex-shrink: 0;
|
|
@@ -934,7 +1086,7 @@ $root-name: ai-chat;
|
|
|
934
1086
|
transition: background-color 0.15s ease;
|
|
935
1087
|
|
|
936
1088
|
&:hover {
|
|
937
|
-
background-color: var(
|
|
1089
|
+
background-color: fn.use-var(bg-color, hover);
|
|
938
1090
|
}
|
|
939
1091
|
}
|
|
940
1092
|
|
|
@@ -1051,7 +1203,7 @@ $root-name: ai-chat;
|
|
|
1051
1203
|
transition: background-color 0.15s ease;
|
|
1052
1204
|
|
|
1053
1205
|
&:hover {
|
|
1054
|
-
background-color: var(
|
|
1206
|
+
background-color: fn.use-var(bg-color, hover);
|
|
1055
1207
|
}
|
|
1056
1208
|
}
|
|
1057
1209
|
|
|
@@ -1148,7 +1300,7 @@ $root-name: ai-chat;
|
|
|
1148
1300
|
color 0.15s ease;
|
|
1149
1301
|
|
|
1150
1302
|
&:hover {
|
|
1151
|
-
background-color: var(
|
|
1303
|
+
background-color: fn.use-var(bg-color, hover);
|
|
1152
1304
|
}
|
|
1153
1305
|
|
|
1154
1306
|
@include m.is(active) {
|
|
@@ -33,33 +33,26 @@
|
|
|
33
33
|
查看面板
|
|
34
34
|
</UButton>
|
|
35
35
|
</div>
|
|
36
|
-
<!--
|
|
36
|
+
<!-- 自定义渲染:meta.render / tool-<name> 插槽优先于通用工具视图 -->
|
|
37
37
|
<component :is="tool.render" v-else-if="tool?.render" :tool-call="toolCall" />
|
|
38
38
|
<component :is="slotBody" v-else-if="slotBody && hasResult" />
|
|
39
39
|
<template v-else>
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
</template>
|
|
44
|
-
|
|
45
|
-
<template v-if="hasResult">
|
|
46
|
-
<div :class="cls.e('tool-call-section')">
|
|
47
|
-
{{ toolCall.status === 'error' ? '错误' : '结果' }}
|
|
48
|
-
</div>
|
|
49
|
-
<pre :class="cls.e('tool-call-code')">{{ displayResult }}</pre>
|
|
50
|
-
</template>
|
|
40
|
+
<ToolJson v-if="prettyArguments" title="参数" :text="prettyArguments" />
|
|
41
|
+
<ToolJson v-if="hasResult" :title="resultTitle" :text="displayResult" />
|
|
42
|
+
<ToolJson v-if="prettyView" title="视图" :text="prettyView" />
|
|
51
43
|
</template>
|
|
52
44
|
</UCollapseItem>
|
|
53
45
|
</template>
|
|
54
46
|
|
|
55
47
|
<script lang="ts" setup>
|
|
56
48
|
import { UButton, UCollapseItem, UIcon } from '@veltra/desktop'
|
|
57
|
-
import { CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from '@veltra/icons/normal'
|
|
58
49
|
import { bem } from '@veltra/utils'
|
|
59
|
-
import { computed, inject, ref, watch
|
|
50
|
+
import { computed, inject, ref, watch } from 'vue'
|
|
60
51
|
|
|
61
52
|
import type { ChatToolCall } from '../../chat/types'
|
|
62
53
|
import { AiChatDIKey } from './di'
|
|
54
|
+
import { resolveToolIcon } from './tool-icons'
|
|
55
|
+
import ToolJson from './tool-json.vue'
|
|
63
56
|
|
|
64
57
|
defineOptions({ name: 'UAiChatToolCall' })
|
|
65
58
|
|
|
@@ -70,7 +63,24 @@ const emit = defineEmits<{ (e: 'respond', approved: boolean): void }>()
|
|
|
70
63
|
const di = inject(AiChatDIKey)
|
|
71
64
|
const cls = di?.cls ?? bem('ai-chat')
|
|
72
65
|
|
|
73
|
-
/**
|
|
66
|
+
/** 把未知值格式成可读 JSON,循环引用等异常时退回 String,不得 throw */
|
|
67
|
+
function formatUnknown(value: unknown): string {
|
|
68
|
+
if (value == null) return ''
|
|
69
|
+
if (typeof value === 'string') {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.stringify(JSON.parse(value), null, 2)
|
|
72
|
+
} catch {
|
|
73
|
+
return value
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return JSON.stringify(value, null, 2) ?? String(value)
|
|
78
|
+
} catch {
|
|
79
|
+
return String(value)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 当前调用对应的工具定义或渲染元信息(session 下忽略 execute / needsConfirm / terminal) */
|
|
74
84
|
const tool = computed(() => di?.tools.value[props.toolCall.name])
|
|
75
85
|
|
|
76
86
|
const isExpanded = ref(false)
|
|
@@ -94,7 +104,7 @@ watch(
|
|
|
94
104
|
() => props.toolCall.status,
|
|
95
105
|
(status) => {
|
|
96
106
|
if (userToggled) return
|
|
97
|
-
if (status === 'running' || status === 'awaiting-confirm') {
|
|
107
|
+
if (status === 'pending' || status === 'running' || status === 'awaiting-confirm') {
|
|
98
108
|
isExpanded.value = true
|
|
99
109
|
} else {
|
|
100
110
|
isExpanded.value = !autoCollapse.value
|
|
@@ -103,17 +113,16 @@ watch(
|
|
|
103
113
|
{ immediate: true }
|
|
104
114
|
)
|
|
105
115
|
|
|
106
|
-
const
|
|
107
|
-
pending:
|
|
108
|
-
running:
|
|
109
|
-
'awaiting-confirm':
|
|
110
|
-
success:
|
|
111
|
-
error:
|
|
112
|
-
rejected:
|
|
116
|
+
const STATUS_CLASS: Record<string, string> = {
|
|
117
|
+
pending: 'is-active',
|
|
118
|
+
running: 'is-active',
|
|
119
|
+
'awaiting-confirm': 'is-warning',
|
|
120
|
+
success: 'is-success',
|
|
121
|
+
error: 'is-danger',
|
|
122
|
+
rejected: 'is-danger'
|
|
113
123
|
}
|
|
114
124
|
|
|
115
|
-
const
|
|
116
|
-
const statusClass = computed(() => STATUS_META[props.toolCall.status]?.class ?? '')
|
|
125
|
+
const statusClass = computed(() => STATUS_CLASS[props.toolCall.status] ?? '')
|
|
117
126
|
|
|
118
127
|
/** 进行中(pending/running):工具名带扫光效果 */
|
|
119
128
|
const isActive = computed(() => {
|
|
@@ -131,8 +140,10 @@ const isSettled = computed(() => {
|
|
|
131
140
|
*/
|
|
132
141
|
const destroyOnCollapse = computed(() => isSettled.value && !isPanelTool.value)
|
|
133
142
|
|
|
134
|
-
/**
|
|
135
|
-
const headerIcon = computed(
|
|
143
|
+
/** 头部图标:meta.icon > 宿主覆盖 > 内置名称规则 > 兜底;状态点走 status class */
|
|
144
|
+
const headerIcon = computed(
|
|
145
|
+
() => tool.value?.icon ?? resolveToolIcon(props.toolCall.name, di?.toolIcons?.value)
|
|
146
|
+
)
|
|
136
147
|
|
|
137
148
|
const parsedArguments = computed<Record<string, unknown> | null>(() => {
|
|
138
149
|
if (!props.toolCall.arguments) return null
|
|
@@ -165,13 +176,14 @@ const hasResult = computed(() => {
|
|
|
165
176
|
return props.toolCall.result != null || props.toolCall.error != null
|
|
166
177
|
})
|
|
167
178
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
const resultTitle = computed(() => (props.toolCall.status === 'error' ? '错误' : '结果'))
|
|
180
|
+
|
|
181
|
+
const displayResult = computed(() => formatUnknown(props.toolCall.error ?? props.toolCall.result))
|
|
182
|
+
|
|
183
|
+
/** 服务端下发的 view,格式化为 JSON;不得丢弃、不得 throw */
|
|
184
|
+
const prettyView = computed(() => {
|
|
185
|
+
if (props.toolCall.view == null) return ''
|
|
186
|
+
return formatUnknown(props.toolCall.view)
|
|
175
187
|
})
|
|
176
188
|
|
|
177
189
|
/** 使用方通过 tool-<name> 插槽自定义该工具的展示(有结果时替换整个 body) */
|