|
@@ -2,7 +2,7 @@
|
|
|
// 单个会话:<AgentChat>(流式输出、审批与提问、上传、定时任务、后台调用都由组件处理)
|
|
// 单个会话:<AgentChat>(流式输出、审批与提问、上传、定时任务、后台调用都由组件处理)
|
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
|
|
import { useRouter } from 'vue-router'
|
|
import { useRouter } from 'vue-router'
|
|
|
-import type { SessionDetailDto } from '@agent-server/client'
|
|
|
|
|
|
|
+import type { ProviderStatusDto, SessionDetailDto } from '@agent-server/client'
|
|
|
import { AgentChat, useChatSession } from '@agent-server/chat-ui'
|
|
import { AgentChat, useChatSession } from '@agent-server/chat-ui'
|
|
|
import TopBar from '../components/TopBar.vue'
|
|
import TopBar from '../components/TopBar.vue'
|
|
|
import { agentClient } from '../lib/config'
|
|
import { agentClient } from '../lib/config'
|
|
@@ -24,6 +24,8 @@ async function refresh() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
void refresh()
|
|
void refresh()
|
|
|
|
|
+const providers = ref<ProviderStatusDto[]>([])
|
|
|
|
|
+void client.providers().then(p => { providers.value = p }).catch(() => {})
|
|
|
// 一轮结束后重新读取标题(首轮结束时服务端会自动起标题)
|
|
// 一轮结束后重新读取标题(首轮结束时服务端会自动起标题)
|
|
|
watch(() => chat.state.runState, s => { if (s === 'idle') void refresh() })
|
|
watch(() => chat.state.runState, s => { if (s === 'idle') void refresh() })
|
|
|
onBeforeUnmount(() => chat.dispose())
|
|
onBeforeUnmount(() => chat.dispose())
|
|
@@ -36,6 +38,25 @@ const subtitle = computed(() => {
|
|
|
return parts.filter(Boolean).join(' · ')
|
|
return parts.filter(Boolean).join(' · ')
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 可选模型来自引擎设置;会话当前的模型不在列表中时(例如 Profile 指定的模型)也保留
|
|
|
|
|
+const provider = computed(() => providers.value.find(p => p.id === detail.value?.provider))
|
|
|
|
|
+const models = computed(() => {
|
|
|
|
|
+ const list = provider.value?.models ?? []
|
|
|
|
|
+ const cur = detail.value?.model
|
|
|
|
|
+ return cur && !list.includes(cur) ? [cur, ...list] : list
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+async function setModel(model: string) {
|
|
|
|
|
+ if (!detail.value || model === detail.value.model) return
|
|
|
|
|
+ try {
|
|
|
|
|
+ await client.sessions.update(props.id, { model })
|
|
|
|
|
+ await refresh()
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ toast((err as Error).message, 'error')
|
|
|
|
|
+ await refresh()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function menu() {
|
|
async function menu() {
|
|
|
if (!detail.value) return
|
|
if (!detail.value) return
|
|
|
const r = await sessionMenu(client, detail.value, [{ value: 'files', label: '文件', icon: 'files' }])
|
|
const r = await sessionMenu(client, detail.value, [{ value: 'files', label: '文件', icon: 'files' }])
|
|
@@ -51,6 +72,42 @@ async function menu() {
|
|
|
<button type="button" class="m-icon" aria-label="更多" @click="menu"><span class="codicon codicon-ellipsis text-[18px]" /></button>
|
|
<button type="button" class="m-icon" aria-label="更多" @click="menu"><span class="codicon codicon-ellipsis text-[18px]" /></button>
|
|
|
</TopBar>
|
|
</TopBar>
|
|
|
<div class="flex-1 min-h-0 pb-[var(--safe-bottom)] bg-[var(--app-primary-background)]">
|
|
<div class="flex-1 min-h-0 pb-[var(--safe-bottom)] bg-[var(--app-primary-background)]">
|
|
|
- <AgentChat :client="client" :session="chat" placeholder="问点什么,或上传文件…" @open-file="f => openFile(client, f)" />
|
|
|
|
|
|
|
+ <AgentChat :client="client" :session="chat" placeholder="问点什么,或上传文件…" @open-file="f => openFile(client, f)">
|
|
|
|
|
+ <template #toolbar>
|
|
|
|
|
+ <label v-if="detail && models.length" class="model-pill">
|
|
|
|
|
+ <span class="codicon codicon-sparkle text-[12px]" />
|
|
|
|
|
+ <select :value="detail.model" :disabled="chat.state.runState !== 'idle'" aria-label="模型" @change="setModel(($event.target as HTMLSelectElement).value)">
|
|
|
|
|
+ <option v-for="m in models" :key="m" :value="m">{{ m }}{{ provider?.modelLabels?.[m] ? ` · ${provider.modelLabels[m]}` : '' }}</option>
|
|
|
|
|
+ </select>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </AgentChat>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.model-pill {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ height: 26px;
|
|
|
|
|
+ padding: 0 8px;
|
|
|
|
|
+ border-radius: 13px;
|
|
|
|
|
+ border: 1px solid var(--app-primary-border-color);
|
|
|
|
|
+ color: var(--app-secondary-foreground);
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.model-pill select {
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ color: inherit;
|
|
|
|
|
+ outline: none;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ max-width: 150px;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+}
|
|
|
|
|
+.model-pill select option { background: var(--app-input-secondary-background); color: var(--app-primary-foreground); }
|
|
|
|
|
+.model-pill select:disabled { opacity: 0.6; }
|
|
|
|
|
+</style>
|