|
@@ -13,15 +13,37 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- 已选会话引用标签 -->
|
|
|
|
|
+ <div v-if="selectedSessions.length > 0" class="session-chips">
|
|
|
|
|
+ <div v-for="session in selectedSessions" :key="session.id" class="session-chip">
|
|
|
|
|
+ <div class="session-chip-icon"></div>
|
|
|
|
|
+ <span class="session-chip-name">{{ session.name }}</span>
|
|
|
|
|
+ <CloseOutlined class="session-chip-remove" @click.stop="removeSession(session.id)" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 已选技能引用标签 -->
|
|
|
|
|
+ <div v-if="selectedSkill" class="skill-chips">
|
|
|
|
|
+ <div class="skill-chip">
|
|
|
|
|
+ <div class="skill-chip-icon"></div>
|
|
|
|
|
+ <span class="skill-chip-name">{{ selectedSkill.display_name }}</span>
|
|
|
|
|
+ <CloseOutlined class="skill-chip-remove" @click.stop="removeSkill()" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<a-textarea
|
|
<a-textarea
|
|
|
|
|
+ ref="textareaRef"
|
|
|
:value="modelValue"
|
|
:value="modelValue"
|
|
|
- @update:value="emit('update:modelValue', $event)"
|
|
|
|
|
|
|
+ @update:value="handleTextareaInput"
|
|
|
:rows="3"
|
|
:rows="3"
|
|
|
class="chat-input"
|
|
class="chat-input"
|
|
|
placeholder="请输入你的问题"
|
|
placeholder="请输入你的问题"
|
|
|
:auto-size="{ minRows: 3, maxRows: 6 }"
|
|
:auto-size="{ minRows: 3, maxRows: 6 }"
|
|
|
|
|
+ @keydown="handleSessionKeydown"
|
|
|
@keydown.enter.prevent="handleEnterKey"
|
|
@keydown.enter.prevent="handleEnterKey"
|
|
|
|
|
+ @keydown.backspace="handleBackspace"
|
|
|
/>
|
|
/>
|
|
|
|
|
+
|
|
|
<div class="input-actions">
|
|
<div class="input-actions">
|
|
|
<div class="action-left">
|
|
<div class="action-left">
|
|
|
<div class="btn-upload-wrapper">
|
|
<div class="btn-upload-wrapper">
|
|
@@ -35,18 +57,22 @@
|
|
|
<div class="popup-item-icon attach-icon"></div>
|
|
<div class="popup-item-icon attach-icon"></div>
|
|
|
<span class="popup-item-text">添加附件</span>
|
|
<span class="popup-item-text">添加附件</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="popup-item" @click="handleInsertMention">
|
|
|
|
|
|
|
+ <!-- <div class="popup-item" @click="handleInsertMention">
|
|
|
<div class="popup-item-icon mention-icon"></div>
|
|
<div class="popup-item-icon mention-icon"></div>
|
|
|
<span class="popup-item-text">插入 @ 提及</span>
|
|
<span class="popup-item-text">插入 @ 提及</span>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </div> -->
|
|
|
<div class="popup-item" @click="handleInsertSession">
|
|
<div class="popup-item" @click="handleInsertSession">
|
|
|
<div class="popup-item-icon session-icon"></div>
|
|
<div class="popup-item-icon session-icon"></div>
|
|
|
<span class="popup-item-text">插入 # 会话</span>
|
|
<span class="popup-item-text">插入 # 会话</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="popup-item" @click="handleInsertCommand">
|
|
|
|
|
|
|
+ <div class="popup-item" @click="handleInsertSkill">
|
|
|
|
|
+ <div class="popup-item-icon skill-icon"></div>
|
|
|
|
|
+ <span class="popup-item-text">插入 $ 技能</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- <div class="popup-item" @click="handleInsertCommand">
|
|
|
<div class="popup-item-icon command-icon"></div>
|
|
<div class="popup-item-icon command-icon"></div>
|
|
|
<span class="popup-item-text">插入 / 命令</span>
|
|
<span class="popup-item-text">插入 / 命令</span>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </div> -->
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="action-divider"></div>
|
|
<div class="action-divider"></div>
|
|
@@ -75,11 +101,11 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <input ref="fileInputRef" type="file" style="display: none" @change="emit('file-change', $event)" accept=".pdf" />
|
|
|
|
|
|
|
+ <input ref="fileInputRef" type="file" style="display: none" @change="emit('file-change', $event)" />
|
|
|
</div>
|
|
</div>
|
|
|
<div class="action-right">
|
|
<div class="action-right">
|
|
|
- <div class="btn-context-wrapper" ref="contextPopupRef">
|
|
|
|
|
- <div class="btn-context" @click="toggleContextPopup" @mouseenter="showContextPopup = true" @mouseleave="showContextPopup = false">
|
|
|
|
|
|
|
+ <div class="btn-context-wrapper" ref="contextPopupRef" @mouseenter="showContextPopup = true" @mouseleave="showContextPopup = false">
|
|
|
|
|
+ <div class="btn-context" @click="toggleContextPopup">
|
|
|
<svg class="context-ring" viewBox="0 0 32 32">
|
|
<svg class="context-ring" viewBox="0 0 32 32">
|
|
|
<circle class="context-ring-bg" cx="16" cy="16" r="13" />
|
|
<circle class="context-ring-bg" cx="16" cy="16" r="13" />
|
|
|
<circle
|
|
<circle
|
|
@@ -105,9 +131,31 @@
|
|
|
<div class="context-popup-bar-track">
|
|
<div class="context-popup-bar-track">
|
|
|
<div class="context-popup-bar-fill" :style="{ width: contextPercent + '%', background: contextColor }"></div>
|
|
<div class="context-popup-bar-fill" :style="{ width: contextPercent + '%', background: contextColor }"></div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="context-breakdown">
|
|
|
|
|
+ <div class="breakdown-item">
|
|
|
|
|
+ <span class="breakdown-label">历史消息</span>
|
|
|
|
|
+ <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.messages) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="breakdown-item">
|
|
|
|
|
+ <span class="breakdown-label">MCP工具</span>
|
|
|
|
|
+ <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.mcp) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="breakdown-item">
|
|
|
|
|
+ <span class="breakdown-label">技能描述</span>
|
|
|
|
|
+ <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.skills) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="breakdown-item">
|
|
|
|
|
+ <span class="breakdown-label">系统提示词</span>
|
|
|
|
|
+ <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.system_prompt) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="breakdown-item">
|
|
|
|
|
+ <span class="breakdown-label">其他</span>
|
|
|
|
|
+ <span class="breakdown-value">{{ formatTokens(props.contextBreakdown.other) }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="action-divider"></div>
|
|
|
|
|
|
|
+ <!-- <div class="action-divider"></div>
|
|
|
<div class="btn-model-wrapper" ref="modelPopupRef">
|
|
<div class="btn-model-wrapper" ref="modelPopupRef">
|
|
|
<a-tooltip title="选择模型">
|
|
<a-tooltip title="选择模型">
|
|
|
<div class="btn-model" @click="toggleModelPopup">
|
|
<div class="btn-model" @click="toggleModelPopup">
|
|
@@ -130,7 +178,7 @@
|
|
|
<div class="model-item-check" :class="{ visible: selectedModel.key === item.key }"></div>
|
|
<div class="model-item-check" :class="{ visible: selectedModel.key === item.key }"></div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
+ </div> -->
|
|
|
<div class="action-divider"></div>
|
|
<div class="action-divider"></div>
|
|
|
<div class="btn-think-wrapper" ref="thinkPopupRef">
|
|
<div class="btn-think-wrapper" ref="thinkPopupRef">
|
|
|
<a-tooltip title="思考级别">
|
|
<a-tooltip title="思考级别">
|
|
@@ -157,8 +205,68 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="action-divider"></div>
|
|
<div class="action-divider"></div>
|
|
|
- <div class="btn-send" :class="{ loading, disabled: !modelValue.trim() }" @click="handleSend">
|
|
|
|
|
- <div class="send-icon"></div>
|
|
|
|
|
|
|
+ <div class="btn-send" :class="{ disabled: !streaming && (!modelValue.trim() || loading) }" @click="handleSendClick">
|
|
|
|
|
+ <div class="send-icon" :class="{ 'stop-icon': streaming }"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 会话列表弹框 -->
|
|
|
|
|
+ <div v-if="showSessionPopup" ref="sessionPopupRef" class="session-popup" @click.stop @pointerdown.stop>
|
|
|
|
|
+ <div class="session-popup-header">
|
|
|
|
|
+ <span>选择会话</span>
|
|
|
|
|
+ <span class="session-count">{{ selectedSessions.length }}/5</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="session-list">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(task, index) in filteredSessionList"
|
|
|
|
|
+ :key="task.id"
|
|
|
|
|
+ class="session-item"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ active: index === activeSessionIndex,
|
|
|
|
|
+ selected: isSessionSelected(task.id),
|
|
|
|
|
+ disabled: (selectedSessions.length >= 5 && !isSessionSelected(task.id)) || task.id === props.currentTaskId,
|
|
|
|
|
+ }"
|
|
|
|
|
+ @click="handleSelectSession(task)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="session-checkbox" :class="{ checked: isSessionSelected(task.id) }">
|
|
|
|
|
+ <span v-if="isSessionSelected(task.id)" class="check-mark">✓</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="session-item-icon"></div>
|
|
|
|
|
+ <span class="session-item-name">{{ task.name }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="filteredSessionList.length === 0" class="session-empty">暂无匹配会话</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="session-popup-footer">
|
|
|
|
|
+ <span v-if="selectedSessions.length >= 5" class="max-hint">最多选择5个会话</span>
|
|
|
|
|
+ <span v-else>点击选择会话(最多5个)</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 技能列表弹框 -->
|
|
|
|
|
+ <div v-if="showSkillPopup" ref="skillPopupRef" class="skill-popup" @click.stop @pointerdown.stop>
|
|
|
|
|
+ <div class="skill-popup-header">
|
|
|
|
|
+ <span>选择技能</span>
|
|
|
|
|
+ <span class="skill-count">0/1</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="skill-list">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(skill, index) in filteredSkillList"
|
|
|
|
|
+ :key="skill.name"
|
|
|
|
|
+ class="skill-item"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ active: index === activeSkillIndex,
|
|
|
|
|
+ selected: isSkillSelected(skill.name),
|
|
|
|
|
+ }"
|
|
|
|
|
+ @click="handleSelectSkill(skill)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="skill-item-checkbox" :class="{ checked: isSkillSelected(skill.name) }">
|
|
|
|
|
+ <span v-if="isSkillSelected(skill.name)" class="check-mark">✓</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="skill-item-icon"></div>
|
|
|
|
|
+ <span class="skill-item-name">{{ skill.display_name }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="filteredSkillList.length === 0" class="skill-empty">暂无匹配技能</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="skill-popup-footer">
|
|
|
|
|
+ <span>点击选择技能(仅可选1个)</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -166,34 +274,53 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
- import { ref, computed } from 'vue';
|
|
|
|
|
|
|
+ import { ref, computed, watch, nextTick, onMounted } from 'vue';
|
|
|
import { onClickOutside } from '@vueuse/core';
|
|
import { onClickOutside } from '@vueuse/core';
|
|
|
import { CloseOutlined } from '@ant-design/icons-vue';
|
|
import { CloseOutlined } from '@ant-design/icons-vue';
|
|
|
import { formatFileSize } from './utils';
|
|
import { formatFileSize } from './utils';
|
|
|
- import type { AttachedFile } from './types';
|
|
|
|
|
|
|
+ import { getSkills } from '../../api';
|
|
|
|
|
+ import type { AttachedFile, Task } from './types';
|
|
|
|
|
|
|
|
const props = withDefaults(
|
|
const props = withDefaults(
|
|
|
defineProps<{
|
|
defineProps<{
|
|
|
modelValue: string;
|
|
modelValue: string;
|
|
|
pendingFile: AttachedFile | null;
|
|
pendingFile: AttachedFile | null;
|
|
|
loading: boolean;
|
|
loading: boolean;
|
|
|
|
|
+ streaming?: boolean;
|
|
|
contextUsed?: number;
|
|
contextUsed?: number;
|
|
|
contextMax?: number;
|
|
contextMax?: number;
|
|
|
|
|
+ contextPercent?: number;
|
|
|
|
|
+ contextBreakdown?: { messages: number; mcp: number; skills: number; system_prompt: number; other: number };
|
|
|
|
|
+ initialThinkLevel?: string;
|
|
|
|
|
+ initialEditMode?: string;
|
|
|
|
|
+ taskList?: Task[];
|
|
|
|
|
+ currentTaskId?: string;
|
|
|
}>(),
|
|
}>(),
|
|
|
{
|
|
{
|
|
|
contextUsed: 0,
|
|
contextUsed: 0,
|
|
|
- contextMax: 200000,
|
|
|
|
|
|
|
+ contextMax: 1000000,
|
|
|
|
|
+ contextPercent: 0,
|
|
|
|
|
+ streaming: false,
|
|
|
|
|
+ contextBreakdown: () => ({ messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 }),
|
|
|
|
|
+ initialThinkLevel: 'off',
|
|
|
|
|
+ initialEditMode: 'full',
|
|
|
|
|
+ taskList: () => [],
|
|
|
|
|
+ currentTaskId: '',
|
|
|
}
|
|
}
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
const emit = defineEmits<{
|
|
|
(e: 'update:modelValue', value: string): void;
|
|
(e: 'update:modelValue', value: string): void;
|
|
|
(e: 'send'): void;
|
|
(e: 'send'): void;
|
|
|
|
|
+ (e: 'stop'): void;
|
|
|
(e: 'file-upload'): void;
|
|
(e: 'file-upload'): void;
|
|
|
(e: 'file-change', event: Event): void;
|
|
(e: 'file-change', event: Event): void;
|
|
|
(e: 'remove-pending-file'): void;
|
|
(e: 'remove-pending-file'): void;
|
|
|
(e: 'model-change', modelKey: string): void;
|
|
(e: 'model-change', modelKey: string): void;
|
|
|
(e: 'think-level-change', levelKey: string): void;
|
|
(e: 'think-level-change', levelKey: string): void;
|
|
|
|
|
+ (e: 'edit-mode-change', modeKey: string): void;
|
|
|
|
|
+ (e: 'update:selectedSessions', sessions: Array<{ id: string; name: string }>): void;
|
|
|
|
|
+ (e: 'update:selectedSkill', skill: { name: string; display_name: string } | null): void;
|
|
|
}>();
|
|
}>();
|
|
|
|
|
|
|
|
const handleSelectModel = (item: ModelOption) => {
|
|
const handleSelectModel = (item: ModelOption) => {
|
|
@@ -211,8 +338,7 @@
|
|
|
const showContextPopup = ref(false);
|
|
const showContextPopup = ref(false);
|
|
|
|
|
|
|
|
const contextPercent = computed(() => {
|
|
const contextPercent = computed(() => {
|
|
|
- if (!props.contextMax) return 0;
|
|
|
|
|
- return Math.min(100, Math.round((props.contextUsed / props.contextMax) * 100));
|
|
|
|
|
|
|
+ return props.contextPercent;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const contextCircumference = computed(() => 2 * Math.PI * 13);
|
|
const contextCircumference = computed(() => 2 * Math.PI * 13);
|
|
@@ -234,6 +360,11 @@
|
|
|
return props.contextMax.toLocaleString();
|
|
return props.contextMax.toLocaleString();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ const formatTokens = (tokens: number) => {
|
|
|
|
|
+ if (tokens >= 10000) return (tokens / 10000).toFixed(1) + '万';
|
|
|
|
|
+ return tokens.toLocaleString();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const toggleContextPopup = () => {
|
|
const toggleContextPopup = () => {
|
|
|
showContextPopup.value = !showContextPopup.value;
|
|
showContextPopup.value = !showContextPopup.value;
|
|
|
};
|
|
};
|
|
@@ -289,6 +420,15 @@
|
|
|
|
|
|
|
|
const selectedThinkLevel = ref<ThinkLevelOption>(thinkLevelOptions[0]);
|
|
const selectedThinkLevel = ref<ThinkLevelOption>(thinkLevelOptions[0]);
|
|
|
|
|
|
|
|
|
|
+ watch(
|
|
|
|
|
+ () => props.initialThinkLevel,
|
|
|
|
|
+ (level) => {
|
|
|
|
|
+ const found = thinkLevelOptions.find((opt) => opt.key === level);
|
|
|
|
|
+ if (found) selectedThinkLevel.value = found;
|
|
|
|
|
+ },
|
|
|
|
|
+ { immediate: true }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
const toggleThinkPopup = () => {
|
|
const toggleThinkPopup = () => {
|
|
|
showThinkPopup.value = !showThinkPopup.value;
|
|
showThinkPopup.value = !showThinkPopup.value;
|
|
|
};
|
|
};
|
|
@@ -313,14 +453,23 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const editModeOptions: EditModeOption[] = [
|
|
const editModeOptions: EditModeOption[] = [
|
|
|
- { key: 'confirm', label: '变更前确认', desc: '改文件前先问我' },
|
|
|
|
|
- { key: 'auto', label: '自动编辑', desc: '自动编辑文件' },
|
|
|
|
|
|
|
+ // { key: 'confirm', label: '变更前确认', desc: '改文件前先问我' },
|
|
|
|
|
+ // { key: 'auto', label: '自动编辑', desc: '自动编辑文件' },
|
|
|
{ key: 'plan', label: '计划模式', desc: '编辑前先出计划' },
|
|
{ key: 'plan', label: '计划模式', desc: '编辑前先出计划' },
|
|
|
{ key: 'full', label: '完全访问', desc: '减少确认次数' },
|
|
{ key: 'full', label: '完全访问', desc: '减少确认次数' },
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
const selectedEditMode = ref<EditModeOption>(editModeOptions[1]);
|
|
const selectedEditMode = ref<EditModeOption>(editModeOptions[1]);
|
|
|
|
|
|
|
|
|
|
+ watch(
|
|
|
|
|
+ () => props.initialEditMode,
|
|
|
|
|
+ (mode) => {
|
|
|
|
|
+ const found = editModeOptions.find((opt) => opt.key === mode);
|
|
|
|
|
+ if (found) selectedEditMode.value = found;
|
|
|
|
|
+ },
|
|
|
|
|
+ { immediate: true }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
onClickOutside(editModePopupRef, () => {
|
|
onClickOutside(editModePopupRef, () => {
|
|
|
showEditModePopup.value = false;
|
|
showEditModePopup.value = false;
|
|
|
});
|
|
});
|
|
@@ -332,6 +481,7 @@
|
|
|
const handleSelectEditMode = (item: EditModeOption) => {
|
|
const handleSelectEditMode = (item: EditModeOption) => {
|
|
|
selectedEditMode.value = item;
|
|
selectedEditMode.value = item;
|
|
|
showEditModePopup.value = false;
|
|
showEditModePopup.value = false;
|
|
|
|
|
+ emit('edit-mode-change', item.key);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
onClickOutside(popupRef, () => {
|
|
onClickOutside(popupRef, () => {
|
|
@@ -351,16 +501,311 @@
|
|
|
showPopup.value = false;
|
|
showPopup.value = false;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // 会话列表弹框
|
|
|
|
|
+ const textareaRef = ref<InstanceType<any>>();
|
|
|
|
|
+ const sessionPopupRef = ref<HTMLElement>();
|
|
|
|
|
+ const showSessionPopup = ref(false);
|
|
|
|
|
+ const activeSessionIndex = ref(0);
|
|
|
|
|
+ const selectedSessions = ref<Array<{ id: string; name: string }>>([]);
|
|
|
|
|
+ const sessionPopupManual = ref(false);
|
|
|
|
|
+ let isRemovingTag = false;
|
|
|
|
|
+ let sessionJustSelected = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 技能列表弹框
|
|
|
|
|
+ interface SkillItem {
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ display_name: string;
|
|
|
|
|
+ path: string;
|
|
|
|
|
+ description: string;
|
|
|
|
|
+ enabled: boolean;
|
|
|
|
|
+ agents: string[];
|
|
|
|
|
+ has_skill_md: boolean;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const skillPopupRef = ref<HTMLElement>();
|
|
|
|
|
+ const showSkillPopup = ref(false);
|
|
|
|
|
+ const activeSkillIndex = ref(0);
|
|
|
|
|
+ const selectedSkill = ref<{ name: string; display_name: string } | null>(null);
|
|
|
|
|
+ const skillPopupManual = ref(false);
|
|
|
|
|
+ const skillList = ref<SkillItem[]>([]);
|
|
|
|
|
+ let skillJustSelected = false;
|
|
|
|
|
+
|
|
|
|
|
+ const fetchSkillList = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getSkills();
|
|
|
|
|
+ skillList.value = res?.skills || [];
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ skillList.value = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ onMounted(() => {
|
|
|
|
|
+ fetchSkillList();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const isSkillSelected = (name: string) => {
|
|
|
|
|
+ return selectedSkill.value?.name === name;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const isSessionSelected = (sessionId: string) => {
|
|
|
|
|
+ return selectedSessions.value.some((s) => s.id === sessionId);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const sessionSearchText = computed(() => {
|
|
|
|
|
+ const val = props.modelValue || '';
|
|
|
|
|
+ const hashIndex = val.lastIndexOf('#');
|
|
|
|
|
+ if (hashIndex === -1) return '';
|
|
|
|
|
+ const afterHash = val.substring(hashIndex + 1);
|
|
|
|
|
+ if (afterHash.includes(' ')) return '';
|
|
|
|
|
+ return afterHash.toLowerCase();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const filteredSessionList = computed(() => {
|
|
|
|
|
+ const list = props.taskList.filter((t) => t.sessionId);
|
|
|
|
|
+ if (!sessionSearchText.value) return list;
|
|
|
|
|
+ return list.filter((t) => t.name.toLowerCase().includes(sessionSearchText.value));
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ watch(filteredSessionList, () => {
|
|
|
|
|
+ activeSessionIndex.value = 0;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ watch(showSessionPopup, (val) => {
|
|
|
|
|
+ if (val) activeSessionIndex.value = 0;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const skillSearchText = computed(() => {
|
|
|
|
|
+ const val = props.modelValue || '';
|
|
|
|
|
+ const dollarIndex = val.lastIndexOf('$');
|
|
|
|
|
+ if (dollarIndex === -1) return '';
|
|
|
|
|
+ const afterDollar = val.substring(dollarIndex + 1);
|
|
|
|
|
+ if (afterDollar.includes(' ')) return '';
|
|
|
|
|
+ return afterDollar.toLowerCase();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const filteredSkillList = computed(() => {
|
|
|
|
|
+ if (!skillSearchText.value) return skillList.value;
|
|
|
|
|
+ return skillList.value.filter(
|
|
|
|
|
+ (s) => s.display_name.toLowerCase().includes(skillSearchText.value) || s.name.toLowerCase().includes(skillSearchText.value)
|
|
|
|
|
+ );
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ watch(filteredSkillList, () => {
|
|
|
|
|
+ activeSkillIndex.value = 0;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ watch(showSkillPopup, (val) => {
|
|
|
|
|
+ if (val) activeSkillIndex.value = 0;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const handleSessionKeydown = (e: KeyboardEvent) => {
|
|
|
|
|
+ if (showSkillPopup.value && filteredSkillList.value.length > 0) {
|
|
|
|
|
+ if (e.key === 'ArrowDown') {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ activeSkillIndex.value = (activeSkillIndex.value + 1) % filteredSkillList.value.length;
|
|
|
|
|
+ } else if (e.key === 'ArrowUp') {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ activeSkillIndex.value = (activeSkillIndex.value - 1 + filteredSkillList.value.length) % filteredSkillList.value.length;
|
|
|
|
|
+ } else if (e.key === 'Enter') {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const skill = filteredSkillList.value[activeSkillIndex.value];
|
|
|
|
|
+ if (skill) {
|
|
|
|
|
+ skillJustSelected = true;
|
|
|
|
|
+ handleSelectSkill(skill);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (showSessionPopup.value && filteredSessionList.value.length > 0) {
|
|
|
|
|
+ if (e.key === 'ArrowDown') {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ activeSessionIndex.value = (activeSessionIndex.value + 1) % filteredSessionList.value.length;
|
|
|
|
|
+ } else if (e.key === 'ArrowUp') {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ activeSessionIndex.value = (activeSessionIndex.value - 1 + filteredSessionList.value.length) % filteredSessionList.value.length;
|
|
|
|
|
+ } else if (e.key === 'Enter') {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const task = filteredSessionList.value[activeSessionIndex.value];
|
|
|
|
|
+ if (task) {
|
|
|
|
|
+ sessionJustSelected = true;
|
|
|
|
|
+ handleSelectSession(task);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const handleInsertSession = () => {
|
|
const handleInsertSession = () => {
|
|
|
showPopup.value = false;
|
|
showPopup.value = false;
|
|
|
|
|
+ sessionPopupManual.value = true;
|
|
|
|
|
+ showSessionPopup.value = true;
|
|
|
|
|
+ const cur = props.modelValue || '';
|
|
|
|
|
+ const needsSpace = cur && !cur.endsWith(' ') ? ' ' : '';
|
|
|
|
|
+ emit('update:modelValue', `${cur}${needsSpace}#`);
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ const el = textareaRef.value?.$el?.querySelector('textarea') || textareaRef.value?.$el;
|
|
|
|
|
+ if (el) el.focus();
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleSelectSession = (task: Task) => {
|
|
|
|
|
+ if (task.id === props.currentTaskId) return;
|
|
|
|
|
+ const idx = selectedSessions.value.findIndex((s) => s.id === task.id);
|
|
|
|
|
+ if (idx >= 0) {
|
|
|
|
|
+ selectedSessions.value.splice(idx, 1);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (selectedSessions.value.length >= 5) return;
|
|
|
|
|
+ selectedSessions.value.push({ id: task.id, name: task.name });
|
|
|
|
|
+ }
|
|
|
|
|
+ removeSearchTag();
|
|
|
|
|
+ emit('update:selectedSessions', [...selectedSessions.value]);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleInsertSkill = () => {
|
|
|
|
|
+ showPopup.value = false;
|
|
|
|
|
+ showSessionPopup.value = false;
|
|
|
|
|
+ skillPopupManual.value = true;
|
|
|
|
|
+ showSkillPopup.value = true;
|
|
|
|
|
+ const cur = props.modelValue || '';
|
|
|
|
|
+ const needsSpace = cur && !cur.endsWith(' ') ? ' ' : '';
|
|
|
|
|
+ emit('update:modelValue', `${cur}${needsSpace}$`);
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ const el = textareaRef.value?.$el?.querySelector('textarea') || textareaRef.value?.$el;
|
|
|
|
|
+ if (el) el.focus();
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleSelectSkill = (skill: SkillItem) => {
|
|
|
|
|
+ selectedSkill.value = { name: skill.name, display_name: skill.display_name };
|
|
|
|
|
+ removeSkillSearchTag();
|
|
|
|
|
+ showSkillPopup.value = false;
|
|
|
|
|
+ skillPopupManual.value = false;
|
|
|
|
|
+ emit('update:selectedSkill', { name: skill.name, display_name: skill.display_name });
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const removeSkillSearchTag = () => {
|
|
|
|
|
+ isRemovingTag = true;
|
|
|
|
|
+ const val = props.modelValue || '';
|
|
|
|
|
+ const dollarIndex = val.lastIndexOf('$');
|
|
|
|
|
+ if (dollarIndex === -1) {
|
|
|
|
|
+ isRemovingTag = false;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ emit('update:modelValue', val.substring(0, dollarIndex).trimEnd());
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ isRemovingTag = false;
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const removeSkill = () => {
|
|
|
|
|
+ selectedSkill.value = null;
|
|
|
|
|
+ emit('update:selectedSkill', null);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const removeSearchTag = () => {
|
|
|
|
|
+ isRemovingTag = true;
|
|
|
|
|
+ const val = props.modelValue || '';
|
|
|
|
|
+ const hashIndex = val.lastIndexOf('#');
|
|
|
|
|
+ if (hashIndex === -1) {
|
|
|
|
|
+ isRemovingTag = false;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ emit('update:modelValue', val.substring(0, hashIndex).trimEnd());
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ isRemovingTag = false;
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ watch(
|
|
|
|
|
+ () => props.modelValue,
|
|
|
|
|
+ (newVal, oldVal) => {
|
|
|
|
|
+ if (isRemovingTag) return;
|
|
|
|
|
+ if (oldVal && !newVal) {
|
|
|
|
|
+ selectedSessions.value = [];
|
|
|
|
|
+ selectedSkill.value = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const removeSession = (sessionId: string) => {
|
|
|
|
|
+ selectedSessions.value = selectedSessions.value.filter((s) => s.id !== sessionId);
|
|
|
|
|
+ emit('update:selectedSessions', [...selectedSessions.value]);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleTextareaInput = (val: string) => {
|
|
|
|
|
+ emit('update:modelValue', val);
|
|
|
|
|
+
|
|
|
|
|
+ // 技能弹框 $ 检测
|
|
|
|
|
+ if (!skillPopupManual.value) {
|
|
|
|
|
+ const dollarIndex = val.lastIndexOf('$');
|
|
|
|
|
+ if (dollarIndex === -1) {
|
|
|
|
|
+ showSkillPopup.value = false;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const afterDollar = val.substring(dollarIndex + 1);
|
|
|
|
|
+ if (afterDollar.includes(' ')) {
|
|
|
|
|
+ showSkillPopup.value = false;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showSkillPopup.value = true;
|
|
|
|
|
+ showSessionPopup.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 会话弹框 # 检测
|
|
|
|
|
+ if (!sessionPopupManual.value) {
|
|
|
|
|
+ const hashIndex = val.lastIndexOf('#');
|
|
|
|
|
+ if (hashIndex === -1) {
|
|
|
|
|
+ showSessionPopup.value = false;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const afterHash = val.substring(hashIndex + 1);
|
|
|
|
|
+ if (afterHash.includes(' ')) {
|
|
|
|
|
+ showSessionPopup.value = false;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showSessionPopup.value = true;
|
|
|
|
|
+ showSkillPopup.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleBackspace = (e: KeyboardEvent) => {
|
|
|
|
|
+ const val = props.modelValue || '';
|
|
|
|
|
+ if (val) return;
|
|
|
|
|
+ if (selectedSkill.value) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ selectedSkill.value = null;
|
|
|
|
|
+ emit('update:selectedSkill', null);
|
|
|
|
|
+ } else if (selectedSessions.value.length > 0) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ selectedSessions.value.pop();
|
|
|
|
|
+ emit('update:selectedSessions', [...selectedSessions.value]);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ onClickOutside(sessionPopupRef, () => {
|
|
|
|
|
+ showSessionPopup.value = false;
|
|
|
|
|
+ sessionPopupManual.value = false;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ onClickOutside(skillPopupRef, () => {
|
|
|
|
|
+ showSkillPopup.value = false;
|
|
|
|
|
+ skillPopupManual.value = false;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
const handleInsertCommand = () => {
|
|
const handleInsertCommand = () => {
|
|
|
showPopup.value = false;
|
|
showPopup.value = false;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const handleEnterKey = (e: KeyboardEvent) => {
|
|
const handleEnterKey = (e: KeyboardEvent) => {
|
|
|
- if (!e.shiftKey && props.modelValue.trim()) {
|
|
|
|
|
|
|
+ if (showSessionPopup.value || showSkillPopup.value) return;
|
|
|
|
|
+ if (sessionJustSelected) {
|
|
|
|
|
+ sessionJustSelected = false;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (skillJustSelected) {
|
|
|
|
|
+ skillJustSelected = false;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!e.shiftKey && props.modelValue.trim() && !props.loading) {
|
|
|
emit('send');
|
|
emit('send');
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
@@ -371,11 +816,20 @@
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const handleSendClick = () => {
|
|
|
|
|
+ if (props.streaming) {
|
|
|
|
|
+ emit('stop');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ handleSend();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
defineExpose({ fileInputRef });
|
|
defineExpose({ fileInputRef });
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
<style scoped lang="less">
|
|
|
.chat-input-area {
|
|
.chat-input-area {
|
|
|
|
|
+ position: relative;
|
|
|
padding: 5px 10px;
|
|
padding: 5px 10px;
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
background-color: #002f4a;
|
|
background-color: #002f4a;
|
|
@@ -582,6 +1036,10 @@
|
|
|
background-image: var(--img-chat-popup-session-icon);
|
|
background-image: var(--img-chat-popup-session-icon);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .skill-icon {
|
|
|
|
|
+ background-image: var(--img-chat-options-skill-icon);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
.command-icon {
|
|
.command-icon {
|
|
|
background-image: var(--img-chat-popup-command-icon);
|
|
background-image: var(--img-chat-popup-command-icon);
|
|
|
}
|
|
}
|
|
@@ -746,6 +1204,15 @@
|
|
|
border-radius: 8px;
|
|
border-radius: 8px;
|
|
|
min-width: 200px;
|
|
min-width: 200px;
|
|
|
padding: 14px 16px;
|
|
padding: 14px 16px;
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: -12px;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ height: 12px;
|
|
|
|
|
+ }
|
|
|
z-index: 1000;
|
|
z-index: 1000;
|
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
|
|
|
|
|
@@ -796,6 +1263,29 @@
|
|
|
transition: width 0.3s ease;
|
|
transition: width 0.3s ease;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .context-breakdown {
|
|
|
|
|
+ margin-top: 10px;
|
|
|
|
|
+ padding-top: 10px;
|
|
|
|
|
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
|
|
+
|
|
|
|
|
+ .breakdown-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 3px 0;
|
|
|
|
|
+
|
|
|
|
|
+ .breakdown-label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #8b949e;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .breakdown-value {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #e0e6ed;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
.btn-model-wrapper {
|
|
.btn-model-wrapper {
|
|
|
position: relative;
|
|
position: relative;
|
|
@@ -996,9 +1486,356 @@
|
|
|
background-image: var(--img-chat-send-icon);
|
|
background-image: var(--img-chat-send-icon);
|
|
|
background-repeat: no-repeat;
|
|
background-repeat: no-repeat;
|
|
|
background-size: 100% 100%;
|
|
background-size: 100% 100%;
|
|
|
|
|
+
|
|
|
|
|
+ &.stop-icon {
|
|
|
|
|
+ background-image: var(--img-chat-stop-icon);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .session-chips {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ margin-bottom: 5px;
|
|
|
|
|
+
|
|
|
|
|
+ .session-chip {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ padding: 3px 6px 3px 4px;
|
|
|
|
|
+ background: rgba(10, 132, 255, 0.15);
|
|
|
|
|
+ border: 1px solid rgba(10, 132, 255, 0.3);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #e0e6ed;
|
|
|
|
|
+ cursor: default;
|
|
|
|
|
+
|
|
|
|
|
+ .session-chip-icon {
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ background-image: var(--img-chat-popup-session-icon);
|
|
|
|
|
+ background-repeat: no-repeat;
|
|
|
|
|
+ background-size: 100% 100%;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .session-chip-name {
|
|
|
|
|
+ max-width: 150px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .session-chip-remove {
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ color: #8b949e;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ padding: 0 2px;
|
|
|
|
|
+ transition: color 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ color: #ff4d4f;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .skill-chips {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ margin-bottom: 5px;
|
|
|
|
|
+
|
|
|
|
|
+ .skill-chip {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ padding: 3px 6px 3px 4px;
|
|
|
|
|
+ background: rgba(10, 132, 255, 0.15);
|
|
|
|
|
+ border: 1px solid rgba(10, 132, 255, 0.3);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #e0e6ed;
|
|
|
|
|
+ cursor: default;
|
|
|
|
|
+
|
|
|
|
|
+ .skill-chip-icon {
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ background-image: var(--img-chat-options-skill-icon);
|
|
|
|
|
+ background-repeat: no-repeat;
|
|
|
|
|
+ background-size: 100% 100%;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .skill-chip-name {
|
|
|
|
|
+ max-width: 150px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .skill-chip-remove {
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ color: #8b949e;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ padding: 0 2px;
|
|
|
|
|
+ transition: color 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ color: #ff4d4f;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .session-popup {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 100%;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ background: #0a2a3f;
|
|
|
|
|
+ border: 1px solid #1a4a6f;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ max-height: 300px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ z-index: 1000;
|
|
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
|
|
|
+
|
|
|
|
|
+ .session-popup-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 10px 14px 8px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #e0e6ed;
|
|
|
|
|
+ border-bottom: 1px solid rgba(63, 80, 106, 0.3);
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ .session-count {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #3a8fd4;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .session-list {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 6px;
|
|
|
|
|
+
|
|
|
|
|
+ .session-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 8px 10px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ transition: background 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: rgba(10, 132, 255, 0.15);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.active {
|
|
|
|
|
+ background: rgba(10, 132, 255, 0.2);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.selected {
|
|
|
|
|
+ background: rgba(10, 132, 255, 0.12);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.disabled {
|
|
|
|
|
+ opacity: 0.4;
|
|
|
|
|
+ cursor: not-allowed;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .session-checkbox {
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ border: 1.5px solid rgba(255, 255, 255, 0.25);
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &.checked {
|
|
|
|
|
+ background: #3a8fd4;
|
|
|
|
|
+ border-color: #3a8fd4;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .check-mark {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .session-item-icon {
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ background-image: var(--img-chat-popup-session-icon);
|
|
|
|
|
+ background-repeat: no-repeat;
|
|
|
|
|
+ background-size: 100% 100%;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .session-item-name {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #e0e6ed;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .session-empty {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: #8b949e;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ padding: 20px 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .session-popup-footer {
|
|
|
|
|
+ padding: 8px 14px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #8b949e;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ border-top: 1px solid rgba(63, 80, 106, 0.3);
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ .max-hint {
|
|
|
|
|
+ color: #faad14;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .skill-popup {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 100%;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ background: #0a2a3f;
|
|
|
|
|
+ border: 1px solid #1a4a6f;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ max-height: 300px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ z-index: 1000;
|
|
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
|
|
|
+
|
|
|
|
|
+ .skill-popup-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 10px 14px 8px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #e0e6ed;
|
|
|
|
|
+ border-bottom: 1px solid rgba(63, 80, 106, 0.3);
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ .skill-count {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #3a8fd4;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .skill-list {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 6px;
|
|
|
|
|
+
|
|
|
|
|
+ .skill-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 8px 10px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ transition: background 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: rgba(10, 132, 255, 0.15);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.active {
|
|
|
|
|
+ background: rgba(10, 132, 255, 0.2);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.selected {
|
|
|
|
|
+ background: rgba(10, 132, 255, 0.12);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .skill-item-checkbox {
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ border: 1.5px solid rgba(255, 255, 255, 0.25);
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &.checked {
|
|
|
|
|
+ background: #3a8fd4;
|
|
|
|
|
+ border-color: #3a8fd4;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .check-mark {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .skill-item-icon {
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ background-image: var(--img-chat-options-skill-icon);
|
|
|
|
|
+ background-repeat: no-repeat;
|
|
|
|
|
+ background-size: 100% 100%;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .skill-item-name {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #e0e6ed;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .skill-empty {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: #8b949e;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ padding: 20px 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .skill-popup-footer {
|
|
|
|
|
+ padding: 8px 14px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #8b949e;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ border-top: 1px solid rgba(63, 80, 106, 0.3);
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|