feat(frontend): update chat page composables and sidebar plan implementation
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -27,7 +27,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
background: linear-gradient(180deg, rgba(7, 12, 24, 0.92), rgba(7, 11, 20, 0.82));
|
background: linear-gradient(180deg, rgba(7, 12, 24, 0.65), rgba(7, 11, 20, 0.55));
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 12px 14px 14px;
|
padding: 12px 14px 14px;
|
||||||
border-top: 1px solid rgba(34, 211, 238, 0.08);
|
border-top: 1px solid rgba(34, 211, 238, 0.08);
|
||||||
background: linear-gradient(180deg, rgba(6, 10, 18, 0.96), rgba(5, 8, 16, 0.98));
|
background: linear-gradient(180deg, rgba(15, 25, 45, 0.4), rgba(10, 20, 40, 0.45));
|
||||||
}
|
}
|
||||||
|
|
||||||
.runtime-grid {
|
.runtime-grid {
|
||||||
@@ -618,8 +618,9 @@
|
|||||||
.conv-sidebar {
|
.conv-sidebar {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
background: var(--bg-panel);
|
background: rgba(15, 25, 45, 0.2);
|
||||||
border-right: 1px solid rgba(0, 245, 212, 0.15);
|
backdrop-filter: blur(16px);
|
||||||
|
border-right: 1px solid rgba(0, 245, 212, 0.12);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
@@ -636,7 +637,8 @@
|
|||||||
.runtime-sidebar {
|
.runtime-sidebar {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
background: var(--bg-panel);
|
background: rgba(15, 25, 45, 0.2);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
border-left: 1px solid var(--border-dim);
|
border-left: 1px solid var(--border-dim);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1701,7 +1703,8 @@
|
|||||||
|
|
||||||
/* ── JARVIS SIDEBAR STYLES ── */
|
/* ── JARVIS SIDEBAR STYLES ── */
|
||||||
.jarvis-sidebar {
|
.jarvis-sidebar {
|
||||||
background: rgba(4, 10, 20, 0.95) !important;
|
background: rgba(4, 10, 20, 0.6) !important;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
padding: 10px !important;
|
padding: 10px !important;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
@@ -2029,10 +2032,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
background: var(--bg-panel) !important;
|
background: rgba(10, 15, 26, 0.6) !important;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
border-right: 1px solid rgba(0, 245, 212, 0.15);
|
border-right: 1px solid rgba(0, 245, 212, 0.15);
|
||||||
box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.3);
|
box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.1);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2131,7 +2135,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.jarvis-sidebar .jarvis-panel {
|
.jarvis-sidebar .jarvis-panel {
|
||||||
background: linear-gradient(180deg, rgba(8, 18, 36, 0.78), rgba(8, 14, 26, 0.62)) !important;
|
background: linear-gradient(180deg, rgba(8, 18, 36, 0.5), rgba(8, 14, 26, 0.4)) !important;
|
||||||
border: 1px solid rgba(56, 189, 248, 0.14) !important;
|
border: 1px solid rgba(56, 189, 248, 0.14) !important;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 12px 10px 11px;
|
padding: 12px 10px 11px;
|
||||||
@@ -2139,7 +2143,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.2);
|
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.1);
|
||||||
clip-path: none !important;
|
clip-path: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2437,7 +2441,7 @@
|
|||||||
|
|
||||||
.jarvis-progress-core strong {
|
.jarvis-progress-core strong {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: 20px;
|
font-size: 16px;
|
||||||
color: #ecfeff;
|
color: #ecfeff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2641,7 +2645,8 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 10px 24px 11px;
|
padding: 10px 24px 11px;
|
||||||
border-bottom: 1px solid rgba(34, 211, 238, 0.08);
|
border-bottom: 1px solid rgba(34, 211, 238, 0.08);
|
||||||
background: linear-gradient(180deg, rgba(5, 10, 19, 0.92), rgba(4, 9, 17, 0.8));
|
background: linear-gradient(180deg, rgba(5, 10, 19, 0.6), rgba(4, 9, 17, 0.5));
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({
|
|||||||
list: vi.fn(),
|
list: vi.fn(),
|
||||||
getMessages: vi.fn(),
|
getMessages: vi.fn(),
|
||||||
deleteConversation: vi.fn(),
|
deleteConversation: vi.fn(),
|
||||||
|
taskCreate: vi.fn(),
|
||||||
settingsGet: vi.fn(),
|
settingsGet: vi.fn(),
|
||||||
upload: vi.fn(),
|
upload: vi.fn(),
|
||||||
systemStatusGet: vi.fn(),
|
systemStatusGet: vi.fn(),
|
||||||
@@ -27,6 +28,12 @@ vi.mock('@/api/settings', () => ({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/api/task', () => ({
|
||||||
|
taskApi: {
|
||||||
|
create: mocks.taskCreate,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/api/system', () => ({
|
vi.mock('@/api/system', () => ({
|
||||||
systemApi: {
|
systemApi: {
|
||||||
getStatus: mocks.systemStatusGet,
|
getStatus: mocks.systemStatusGet,
|
||||||
@@ -57,6 +64,14 @@ describe('useChatView orchestration state', () => {
|
|||||||
mocks.list.mockResolvedValue({ data: [] })
|
mocks.list.mockResolvedValue({ data: [] })
|
||||||
mocks.getMessages.mockResolvedValue({ data: [] })
|
mocks.getMessages.mockResolvedValue({ data: [] })
|
||||||
mocks.deleteConversation.mockResolvedValue(undefined)
|
mocks.deleteConversation.mockResolvedValue(undefined)
|
||||||
|
mocks.taskCreate.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
id: 'task-1',
|
||||||
|
title: '创建测试任务',
|
||||||
|
status: 'todo',
|
||||||
|
dispatch_status: 'idle',
|
||||||
|
},
|
||||||
|
})
|
||||||
mocks.settingsGet.mockResolvedValue({
|
mocks.settingsGet.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
llm_config: {
|
llm_config: {
|
||||||
@@ -86,7 +101,7 @@ describe('useChatView orchestration state', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('derives telemetry state from real system status and session events without persisting it to message history', async () => {
|
it('derives telemetry state from real system status and session events without persisting it to message history', async () => {
|
||||||
mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, handlers) => {
|
mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, _runtime, handlers) => {
|
||||||
handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' })
|
handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' })
|
||||||
handlers.onProgress?.({
|
handlers.onProgress?.({
|
||||||
stage: 'tool',
|
stage: 'tool',
|
||||||
@@ -126,7 +141,7 @@ describe('useChatView orchestration state', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('keeps the orchestration panel persistent and confines thinking state to the side panel', async () => {
|
it('keeps the orchestration panel persistent and confines thinking state to the side panel', async () => {
|
||||||
mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, handlers) => {
|
mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, _runtime, handlers) => {
|
||||||
handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' })
|
handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' })
|
||||||
handlers.onProgress?.({
|
handlers.onProgress?.({
|
||||||
stage: 'planning',
|
stage: 'planning',
|
||||||
@@ -167,7 +182,7 @@ describe('useChatView orchestration state', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('surfaces schedule fulfillment progress when chat creates a reminder', async () => {
|
it('surfaces schedule fulfillment progress when chat creates a reminder', async () => {
|
||||||
mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, handlers) => {
|
mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, _runtime, handlers) => {
|
||||||
handlers.onMetadata?.({ conversation_id: 'conv-2', message_id: 'msg-2' })
|
handlers.onMetadata?.({ conversation_id: 'conv-2', message_id: 'msg-2' })
|
||||||
handlers.onProgress?.({
|
handlers.onProgress?.({
|
||||||
stage: 'tool',
|
stage: 'tool',
|
||||||
@@ -194,4 +209,88 @@ describe('useChatView orchestration state', () => {
|
|||||||
|
|
||||||
expect(view.store.messages.at(-1)?.content).toBe('已经帮你建好提醒。')
|
expect(view.store.messages.at(-1)?.content).toBe('已经帮你建好提醒。')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sends jarvis as the default runtime', async () => {
|
||||||
|
mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, _runtime, handlers) => {
|
||||||
|
handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' })
|
||||||
|
handlers.onChunk?.({ content: '最终回复' })
|
||||||
|
})
|
||||||
|
|
||||||
|
const view = useChatView()
|
||||||
|
view.inputMessage.value = '测试默认 runtime'
|
||||||
|
|
||||||
|
await view.sendMessage()
|
||||||
|
|
||||||
|
expect(mocks.chatStream).toHaveBeenCalledWith(
|
||||||
|
'测试默认 runtime',
|
||||||
|
undefined,
|
||||||
|
[],
|
||||||
|
undefined,
|
||||||
|
'jarvis',
|
||||||
|
expect.any(Object),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends selected hermes runtime', async () => {
|
||||||
|
mocks.chatStream.mockImplementation(async (_message, _conversationId, _fileIds, _modelName, _runtime, handlers) => {
|
||||||
|
handlers.onMetadata?.({ conversation_id: 'conv-1', message_id: 'msg-1' })
|
||||||
|
handlers.onChunk?.({ content: 'Hermes 回复' })
|
||||||
|
})
|
||||||
|
|
||||||
|
const view = useChatView()
|
||||||
|
view.selectedRuntime.value = 'hermes'
|
||||||
|
view.inputMessage.value = '测试 hermes runtime'
|
||||||
|
|
||||||
|
await view.sendMessage()
|
||||||
|
|
||||||
|
expect(mocks.chatStream).toHaveBeenCalledWith(
|
||||||
|
'测试 hermes runtime',
|
||||||
|
undefined,
|
||||||
|
[],
|
||||||
|
undefined,
|
||||||
|
'hermes',
|
||||||
|
expect.any(Object),
|
||||||
|
)
|
||||||
|
expect(view.store.messages.at(-1)?.model).toBe('hermes')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates a task directly from /task slash command', async () => {
|
||||||
|
const view = useChatView()
|
||||||
|
view.inputMessage.value = '/task 创建测试任务'
|
||||||
|
|
||||||
|
await view.sendMessage()
|
||||||
|
|
||||||
|
expect(mocks.taskCreate).toHaveBeenCalledWith({
|
||||||
|
title: '创建测试任务',
|
||||||
|
source: 'chat',
|
||||||
|
conversation_id: undefined,
|
||||||
|
assignee_type: undefined,
|
||||||
|
dispatch_to_commander: false,
|
||||||
|
})
|
||||||
|
expect(view.store.messages.at(-1)?.content).toContain('已创建任务:创建测试任务')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates and dispatches a task directly from /task@commander slash command', async () => {
|
||||||
|
mocks.taskCreate.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
id: 'task-2',
|
||||||
|
title: '派发测试任务',
|
||||||
|
status: 'todo',
|
||||||
|
dispatch_status: 'queued',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const view = useChatView()
|
||||||
|
view.inputMessage.value = '/task@commander 派发测试任务'
|
||||||
|
|
||||||
|
await view.sendMessage()
|
||||||
|
|
||||||
|
expect(mocks.taskCreate).toHaveBeenCalledWith({
|
||||||
|
title: '派发测试任务',
|
||||||
|
source: 'chat',
|
||||||
|
conversation_id: undefined,
|
||||||
|
assignee_type: 'commander',
|
||||||
|
dispatch_to_commander: true,
|
||||||
|
})
|
||||||
|
expect(view.store.messages.at(-1)?.content).toContain('已创建并派发任务:派发测试任务')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
import {
|
import {
|
||||||
conversationApi,
|
conversationApi,
|
||||||
type ChatProgressEvent,
|
type ChatProgressEvent,
|
||||||
|
type ChatRuntime,
|
||||||
type Message,
|
type Message,
|
||||||
} from '@/api/conversation'
|
} from '@/api/conversation'
|
||||||
import { documentApi } from '@/api/document'
|
import { documentApi } from '@/api/document'
|
||||||
import { settingsApi, type LLMModelConfig } from '@/api/settings'
|
import { settingsApi, type LLMModelConfig } from '@/api/settings'
|
||||||
import { systemApi } from '@/api/system'
|
import { systemApi } from '@/api/system'
|
||||||
|
import { taskApi } from '@/api/task'
|
||||||
|
|
||||||
export interface SelectedFile {
|
export interface SelectedFile {
|
||||||
id: string
|
id: string
|
||||||
@@ -94,6 +96,17 @@ const ORCHESTRATION_EVENT_STORAGE_KEY = 'jarvis.chat.orchestration.events'
|
|||||||
const ORCHESTRATION_EVENT_GROUP_LIMIT = 40
|
const ORCHESTRATION_EVENT_GROUP_LIMIT = 40
|
||||||
const ORCHESTRATION_EVENT_ITEM_LIMIT = 24
|
const ORCHESTRATION_EVENT_ITEM_LIMIT = 24
|
||||||
|
|
||||||
|
function isTaskSlashCommand(value: string) {
|
||||||
|
return value.startsWith('/task ') || value === '/task' || value.startsWith('/task@commander ') || value === '/task@commander'
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTaskSlashCommand(value: string) {
|
||||||
|
const isCommander = value.startsWith('/task@commander')
|
||||||
|
const prefix = isCommander ? '/task@commander' : '/task'
|
||||||
|
const title = value.slice(prefix.length).trim()
|
||||||
|
return { isCommander, title }
|
||||||
|
}
|
||||||
|
|
||||||
export function useChatView() {
|
export function useChatView() {
|
||||||
const store = useConversationStore()
|
const store = useConversationStore()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@@ -151,6 +164,7 @@ export function useChatView() {
|
|||||||
agentCount: 0,
|
agentCount: 0,
|
||||||
})
|
})
|
||||||
const selectedModel = computed(() => chatModels.value.find((model) => model.name === selectedModelName.value) ?? null)
|
const selectedModel = computed(() => chatModels.value.find((model) => model.name === selectedModelName.value) ?? null)
|
||||||
|
const selectedRuntime = ref<ChatRuntime>('jarvis')
|
||||||
let systemTelemetryTimer: ReturnType<typeof setInterval> | null = null
|
let systemTelemetryTimer: ReturnType<typeof setInterval> | null = null
|
||||||
let sessionTelemetryTimer: ReturnType<typeof setInterval> | null = null
|
let sessionTelemetryTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
@@ -467,6 +481,51 @@ export function useChatView() {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
|
if (isTaskSlashCommand(text)) {
|
||||||
|
try {
|
||||||
|
const { isCommander, title } = parseTaskSlashCommand(text)
|
||||||
|
if (!title) {
|
||||||
|
throw new Error(isCommander ? '用法:/task@commander 任务标题' : '用法:/task 任务标题')
|
||||||
|
}
|
||||||
|
const response = await taskApi.create({
|
||||||
|
title,
|
||||||
|
source: 'chat',
|
||||||
|
conversation_id: store.currentConversationId ?? undefined,
|
||||||
|
assignee_type: isCommander ? 'commander' : undefined,
|
||||||
|
dispatch_to_commander: isCommander,
|
||||||
|
})
|
||||||
|
const task = response.data
|
||||||
|
const reply = isCommander
|
||||||
|
? `已创建并派发任务:${task.title}\n状态:${task.dispatch_status.toUpperCase()}`
|
||||||
|
: `已创建任务:${task.title}\n状态:${task.status.toUpperCase()}`
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new CustomEvent('jarvis:today-status-refresh'))
|
||||||
|
}
|
||||||
|
store.addMessage({
|
||||||
|
id: `assistant-${Date.now()}`,
|
||||||
|
role: 'assistant',
|
||||||
|
content: reply,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
finalizeOrchestration('complete', isCommander ? '任务已创建并派发' : '任务已创建')
|
||||||
|
} catch (error: any) {
|
||||||
|
const content = error?.message || '任务创建失败'
|
||||||
|
finalizeOrchestration('error', content)
|
||||||
|
store.addMessage({
|
||||||
|
id: `err-${Date.now()}`,
|
||||||
|
role: 'assistant',
|
||||||
|
content,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isTyping.value = false
|
||||||
|
isSending.value = false
|
||||||
|
selectedFiles.value = []
|
||||||
|
await nextTick()
|
||||||
|
scrollToBottom()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let finalConversationId = previousConversationId
|
let finalConversationId = previousConversationId
|
||||||
let finalMessageId = ''
|
let finalMessageId = ''
|
||||||
let finalContent = ''
|
let finalContent = ''
|
||||||
@@ -478,6 +537,7 @@ export function useChatView() {
|
|||||||
previousConversationId ?? undefined,
|
previousConversationId ?? undefined,
|
||||||
attachments.map((file) => file.id),
|
attachments.map((file) => file.id),
|
||||||
selectedModelName.value || undefined,
|
selectedModelName.value || undefined,
|
||||||
|
selectedRuntime.value,
|
||||||
{
|
{
|
||||||
onMetadata(payload) {
|
onMetadata(payload) {
|
||||||
finalConversationId = payload.conversation_id
|
finalConversationId = payload.conversation_id
|
||||||
@@ -517,7 +577,9 @@ export function useChatView() {
|
|||||||
id: finalMessageId || `assistant-${Date.now()}`,
|
id: finalMessageId || `assistant-${Date.now()}`,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: finalContent || '抱歉,我暂时没有生成可用回复。',
|
content: finalContent || '抱歉,我暂时没有生成可用回复。',
|
||||||
model: selectedModelName.value || selectedModel.value?.name || 'jarvis',
|
model: selectedRuntime.value === 'hermes'
|
||||||
|
? 'hermes'
|
||||||
|
: (selectedModelName.value || selectedModel.value?.name || 'jarvis'),
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
if (finalConversationId && previousConversationId === null) {
|
if (finalConversationId && previousConversationId === null) {
|
||||||
@@ -714,6 +776,7 @@ export function useChatView() {
|
|||||||
chatModels,
|
chatModels,
|
||||||
selectedModelName,
|
selectedModelName,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
|
selectedRuntime,
|
||||||
isLoadingModels,
|
isLoadingModels,
|
||||||
conversationsError,
|
conversationsError,
|
||||||
thinkingState,
|
thinkingState,
|
||||||
|
|||||||
@@ -1,23 +1,90 @@
|
|||||||
import { computed, onMounted, ref, watch, type Ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref, watch, type Ref } from 'vue'
|
||||||
import { CornerDownLeft, Database, Sparkles, Sun, ListTodo } from 'lucide-vue-next'
|
import { CornerDownLeft, Database, Sparkles, Sun, ListTodo } from 'lucide-vue-next'
|
||||||
import { scheduleCenterApi, type ScheduleCenterDateResponse, type ScheduleCenterDaySummary } from '@/api/scheduleCenter'
|
import {
|
||||||
|
scheduleCenterApi,
|
||||||
|
type ScheduleCenterCommanderSummary,
|
||||||
|
type ScheduleCenterDateResponse,
|
||||||
|
type ScheduleCenterDaySummary,
|
||||||
|
} from '@/api/scheduleCenter'
|
||||||
|
import { taskApi, type Task, type TaskDispatchStatus, type TaskQuadrant } from '@/api/task'
|
||||||
import type { Conversation } from '@/api/conversation'
|
import type { Conversation } from '@/api/conversation'
|
||||||
|
|
||||||
export interface SidebarFocusItem {
|
export interface SidebarFocusItem {
|
||||||
id: string; label: string; title: string; meta: string; tone: 'done' | 'doing' | 'pending'
|
id: string
|
||||||
|
label: string
|
||||||
|
title: string
|
||||||
|
meta: string
|
||||||
|
tone: 'done' | 'doing' | 'pending'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarNewsItem {
|
export interface SidebarNewsItem {
|
||||||
id: string; title: string; meta: string
|
id: string
|
||||||
|
title: string
|
||||||
|
meta: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TodayStatusQuadrantViewTask {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
completed: boolean
|
||||||
|
status: string
|
||||||
|
dispatchStatus: string
|
||||||
|
assigneeLabel: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TodayStatusQuadrantView {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
color: string
|
||||||
|
glowColor: string
|
||||||
|
icon: string
|
||||||
|
tasks: TodayStatusQuadrantViewTask[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sidebarCollapsedModules = [
|
export const sidebarCollapsedModules = [
|
||||||
{ id: 'calendar', label: '日历', icon: Sun },
|
{ id: 'calendar', label: 'Calendar', icon: Sun },
|
||||||
{ id: 'status', label: '计划', icon: Database },
|
{ id: 'status', label: 'Status', icon: Database },
|
||||||
{ id: 'focus', label: '重点', icon: Sparkles },
|
{ id: 'focus', label: 'Focus', icon: Sparkles },
|
||||||
{ id: 'kanban', label: '待办', icon: ListTodo },
|
{ id: 'kanban', label: 'Issues', icon: ListTodo },
|
||||||
{ id: 'review', label: '复盘', icon: CornerDownLeft },
|
{ id: 'review', label: 'Review', icon: CornerDownLeft },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ISSUE_QUADRANT_META: Record<TaskQuadrant, Omit<TodayStatusQuadrantView, 'tasks'>> = {
|
||||||
|
'urgent-important': {
|
||||||
|
id: 'urgent-important',
|
||||||
|
title: 'Important & Urgent',
|
||||||
|
subtitle: 'CRITICAL',
|
||||||
|
color: '#ff4757',
|
||||||
|
glowColor: 'rgba(255, 71, 87, 0.4)',
|
||||||
|
icon: '!',
|
||||||
|
},
|
||||||
|
'not-urgent-important': {
|
||||||
|
id: 'not-urgent-important',
|
||||||
|
title: 'Important & Planned',
|
||||||
|
subtitle: 'PLANNED',
|
||||||
|
color: '#ffd93d',
|
||||||
|
glowColor: 'rgba(255, 217, 61, 0.4)',
|
||||||
|
icon: 'P',
|
||||||
|
},
|
||||||
|
'urgent-not-important': {
|
||||||
|
id: 'urgent-not-important',
|
||||||
|
title: 'Urgent & Delegated',
|
||||||
|
subtitle: 'DELEGATE',
|
||||||
|
color: '#00d4ff',
|
||||||
|
glowColor: 'rgba(0, 212, 255, 0.4)',
|
||||||
|
icon: 'D',
|
||||||
|
},
|
||||||
|
'not-urgent-not-important': {
|
||||||
|
id: 'not-urgent-not-important',
|
||||||
|
title: 'Backlog / Low Priority',
|
||||||
|
subtitle: 'BACKLOG',
|
||||||
|
color: '#6bcf7f',
|
||||||
|
glowColor: 'rgba(107, 207, 127, 0.4)',
|
||||||
|
icon: 'B',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export function formatDateKey(date: Date) {
|
export function formatDateKey(date: Date) {
|
||||||
const year = date.getFullYear()
|
const year = date.getFullYear()
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
@@ -31,6 +98,80 @@ function formatMonthKey(date: Date) {
|
|||||||
return `${year}-${month}`
|
return `${year}-${month}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toneFromStatus(status: string) {
|
||||||
|
if (status === 'done' || status === 'completed') return 'done' as const
|
||||||
|
if (status === 'in_progress' || status === 'running' || status === 'queued') return 'doing' as const
|
||||||
|
return 'pending' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusLabelByQuadrant(quadrant?: string | null) {
|
||||||
|
if (!quadrant) return 'Task'
|
||||||
|
if (quadrant === 'urgent-important') return 'Critical'
|
||||||
|
if (quadrant === 'not-urgent-important') return 'Planned'
|
||||||
|
if (quadrant === 'urgent-not-important') return 'Delegated'
|
||||||
|
return 'Backlog'
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusMeta(status: string, dispatchStatus: string, assigneeType?: string | null, assigneeId?: string | null) {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
todo: 'Pending',
|
||||||
|
in_progress: 'In progress',
|
||||||
|
done: 'Done',
|
||||||
|
cancelled: 'Cancelled',
|
||||||
|
}
|
||||||
|
const dispatchMap: Record<string, string> = {
|
||||||
|
idle: 'Not dispatched',
|
||||||
|
queued: 'Queued',
|
||||||
|
running: 'Running',
|
||||||
|
completed: 'Completed',
|
||||||
|
failed: 'Failed',
|
||||||
|
}
|
||||||
|
const statusLabel = dispatchStatus !== 'idle' ? dispatchMap[dispatchStatus] : (statusMap[status] || 'Pending')
|
||||||
|
if (assigneeType && assigneeId) return `${statusLabel} / ${assigneeType}:${assigneeId}`
|
||||||
|
if (assigneeType) return `${statusLabel} / ${assigneeType}`
|
||||||
|
return statusLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveIssueQuadrant(task: Task): TaskQuadrant {
|
||||||
|
if (task.quadrant) return task.quadrant
|
||||||
|
if (task.priority === 'high' || task.priority === 'urgent') return 'urgent-important'
|
||||||
|
if (task.status === 'in_progress') return 'not-urgent-important'
|
||||||
|
if (task.priority === 'medium') return 'urgent-not-important'
|
||||||
|
return 'not-urgent-not-important'
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIssueCommanderSummary(tasks: Task[]): ScheduleCenterCommanderSummary {
|
||||||
|
const summary: ScheduleCenterCommanderSummary = {
|
||||||
|
total: 0,
|
||||||
|
queued: 0,
|
||||||
|
running: 0,
|
||||||
|
completed: 0,
|
||||||
|
failed: 0,
|
||||||
|
overall_status: 'idle',
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
const state = task.dispatch_status as TaskDispatchStatus
|
||||||
|
if (state === 'idle') return
|
||||||
|
|
||||||
|
summary.total += 1
|
||||||
|
if (state === 'queued') summary.queued += 1
|
||||||
|
if (state === 'running') summary.running += 1
|
||||||
|
if (state === 'completed') summary.completed += 1
|
||||||
|
if (state === 'failed') summary.failed += 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (summary.running > 0) {
|
||||||
|
summary.overall_status = 'running'
|
||||||
|
} else if (summary.queued > 0) {
|
||||||
|
summary.overall_status = 'queued'
|
||||||
|
} else if (summary.failed > 0 && summary.completed === 0) {
|
||||||
|
summary.overall_status = 'failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
export function useSidebarPlan(
|
export function useSidebarPlan(
|
||||||
clientTimeRef: { value: Date },
|
clientTimeRef: { value: Date },
|
||||||
loadDailyDigestFn: () => void,
|
loadDailyDigestFn: () => void,
|
||||||
@@ -38,9 +179,9 @@ export function useSidebarPlan(
|
|||||||
) {
|
) {
|
||||||
const todayPlanDetail = ref<ScheduleCenterDateResponse | null>(null)
|
const todayPlanDetail = ref<ScheduleCenterDateResponse | null>(null)
|
||||||
const monthPlanDays = ref<ScheduleCenterDaySummary[]>([])
|
const monthPlanDays = ref<ScheduleCenterDaySummary[]>([])
|
||||||
|
const issueTasks = ref<Task[]>([])
|
||||||
const selectedDate = ref<string | null>(null)
|
const selectedDate = ref<string | null>(null)
|
||||||
|
|
||||||
// Build a map of date -> has conversation
|
|
||||||
const conversationDateMap = computed(() => {
|
const conversationDateMap = computed(() => {
|
||||||
const map = new Map<string, boolean>()
|
const map = new Map<string, boolean>()
|
||||||
const conversations = Array.isArray(conversationsRef) ? conversationsRef : (conversationsRef.value ?? [])
|
const conversations = Array.isArray(conversationsRef) ? conversationsRef : (conversationsRef.value ?? [])
|
||||||
@@ -53,6 +194,24 @@ export function useSidebarPlan(
|
|||||||
const todayDateKey = computed(() => formatDateKey(clientTimeRef.value))
|
const todayDateKey = computed(() => formatDateKey(clientTimeRef.value))
|
||||||
const monthPlanSummaryMap = computed(() => new Map(monthPlanDays.value.map((item) => [item.date, item])))
|
const monthPlanSummaryMap = computed(() => new Map(monthPlanDays.value.map((item) => [item.date, item])))
|
||||||
|
|
||||||
|
const issueStatusQuadrants = computed<TodayStatusQuadrantView[]>(() => (
|
||||||
|
Object.values(ISSUE_QUADRANT_META).map((meta) => ({
|
||||||
|
...meta,
|
||||||
|
tasks: issueTasks.value
|
||||||
|
.filter((task) => deriveIssueQuadrant(task) === meta.id)
|
||||||
|
.map((task) => ({
|
||||||
|
id: task.id,
|
||||||
|
title: task.title,
|
||||||
|
completed: task.status === 'done',
|
||||||
|
status: task.status,
|
||||||
|
dispatchStatus: task.dispatch_status,
|
||||||
|
assigneeLabel: task.assignee_id ? `${task.assignee_type ?? 'owner'}:${task.assignee_id}` : (task.assignee_type ?? 'unassigned'),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
))
|
||||||
|
|
||||||
|
const issueCommanderSummary = computed<ScheduleCenterCommanderSummary>(() => buildIssueCommanderSummary(issueTasks.value))
|
||||||
|
|
||||||
const calendarCells = computed(() => {
|
const calendarCells = computed(() => {
|
||||||
const year = clientTimeRef.value.getFullYear()
|
const year = clientTimeRef.value.getFullYear()
|
||||||
const month = clientTimeRef.value.getMonth()
|
const month = clientTimeRef.value.getMonth()
|
||||||
@@ -60,9 +219,11 @@ export function useSidebarPlan(
|
|||||||
const firstDayOffset = (new Date(year, month, 1).getDay() + 6) % 7
|
const firstDayOffset = (new Date(year, month, 1).getDay() + 6) % 7
|
||||||
const todayKey = todayDateKey.value
|
const todayKey = todayDateKey.value
|
||||||
const cells: Array<{ key: string; value: number | null; active: boolean; busy: boolean; selected: boolean; hasConversation: boolean }> = []
|
const cells: Array<{ key: string; value: number | null; active: boolean; busy: boolean; selected: boolean; hasConversation: boolean }> = []
|
||||||
|
|
||||||
for (let index = 0; index < firstDayOffset; index += 1) {
|
for (let index = 0; index < firstDayOffset; index += 1) {
|
||||||
cells.push({ key: `blank-start-${index}`, value: null, active: false, busy: false, selected: false, hasConversation: false })
|
cells.push({ key: `blank-start-${index}`, value: null, active: false, busy: false, selected: false, hasConversation: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let day = 1; day <= daysInMonth; day += 1) {
|
for (let day = 1; day <= daysInMonth; day += 1) {
|
||||||
const monthDate = new Date(year, month, day)
|
const monthDate = new Date(year, month, day)
|
||||||
const dateKey = formatDateKey(monthDate)
|
const dateKey = formatDateKey(monthDate)
|
||||||
@@ -71,30 +232,21 @@ export function useSidebarPlan(
|
|||||||
const hasConv = conversationDateMap.value.get(dateKey) || false
|
const hasConv = conversationDateMap.value.get(dateKey) || false
|
||||||
cells.push({ key: dateKey, value: day, active: dateKey === todayKey, busy, selected: dateKey === selectedDate.value, hasConversation: hasConv })
|
cells.push({ key: dateKey, value: day, active: dateKey === todayKey, busy, selected: dateKey === selectedDate.value, hasConversation: hasConv })
|
||||||
}
|
}
|
||||||
|
|
||||||
while (cells.length % 7 !== 0) {
|
while (cells.length % 7 !== 0) {
|
||||||
cells.push({ key: `blank-end-${cells.length}`, value: null, active: false, busy: false, selected: false, hasConversation: false })
|
cells.push({ key: `blank-end-${cells.length}`, value: null, active: false, busy: false, selected: false, hasConversation: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
return cells
|
return cells
|
||||||
})
|
})
|
||||||
|
|
||||||
const calendarYear = computed(() => clientTimeRef.value.getFullYear())
|
const calendarYear = computed(() => clientTimeRef.value.getFullYear())
|
||||||
const calendarMonth = computed(() => clientTimeRef.value.getMonth() + 1)
|
const calendarMonth = computed(() => clientTimeRef.value.getMonth() + 1)
|
||||||
|
|
||||||
const todayPlanCounters = computed(() => {
|
const issueStatusCounters = computed(() => {
|
||||||
const detail = todayPlanDetail.value
|
const done = issueTasks.value.filter((item) => item.status === 'done').length
|
||||||
if (!detail) return { done: 0, doing: 0, pending: 0, total: 0, completion: 0 }
|
const doing = issueTasks.value.filter((item) => item.status === 'in_progress').length
|
||||||
const todoDone = detail.todos.filter((item) => item.is_completed).length
|
const pending = issueTasks.value.filter((item) => item.status === 'todo').length
|
||||||
const todoPending = detail.todos.filter((item) => !item.is_completed).length
|
|
||||||
const taskDone = detail.tasks.filter((item) => item.status === 'done').length
|
|
||||||
const taskDoing = detail.tasks.filter((item) => item.status === 'in_progress').length
|
|
||||||
const taskPending = detail.tasks.filter((item) => item.status === 'todo').length
|
|
||||||
const goalDone = detail.goals.filter((item) => item.status === 'done').length
|
|
||||||
const goalPending = detail.goals.filter((item) => item.status !== 'done').length
|
|
||||||
const reminderDone = detail.reminders.filter((item) => item.status === 'done' || item.is_dismissed).length
|
|
||||||
const reminderPending = detail.reminders.filter((item) => item.status !== 'done' && !item.is_dismissed).length
|
|
||||||
const done = todoDone + taskDone + goalDone + reminderDone
|
|
||||||
const doing = taskDoing
|
|
||||||
const pending = todoPending + taskPending + goalPending + reminderPending
|
|
||||||
const total = done + doing + pending
|
const total = done + doing + pending
|
||||||
return { done, doing, pending, total, completion: total > 0 ? Math.round((done / total) * 100) : 0 }
|
return { done, doing, pending, total, completion: total > 0 ? Math.round((done / total) * 100) : 0 }
|
||||||
})
|
})
|
||||||
@@ -114,89 +266,80 @@ export function useSidebarPlan(
|
|||||||
))
|
))
|
||||||
|
|
||||||
const sidebarWeekLabels = [
|
const sidebarWeekLabels = [
|
||||||
{ label: '一', isWeekend: false },
|
{ label: 'M', isWeekend: false },
|
||||||
{ label: '二', isWeekend: false },
|
{ label: 'T', isWeekend: false },
|
||||||
{ label: '三', isWeekend: false },
|
{ label: 'W', isWeekend: false },
|
||||||
{ label: '四', isWeekend: false },
|
{ label: 'T', isWeekend: false },
|
||||||
{ label: '五', isWeekend: false },
|
{ label: 'F', isWeekend: false },
|
||||||
{ label: '六', isWeekend: true },
|
{ label: 'S', isWeekend: true },
|
||||||
{ label: '日', isWeekend: true },
|
{ label: 'S', isWeekend: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
const sidebarStatusHeadline = computed(() => (
|
const sidebarStatusHeadline = computed(() => '')
|
||||||
todayPlanCounters.value.total
|
|
||||||
? `今日共 ${todayPlanCounters.value.total} 项计划,已完成 ${todayPlanCounters.value.done} 项`
|
|
||||||
: ''
|
|
||||||
))
|
|
||||||
|
|
||||||
const sidebarStatusBreakdown = computed(() => [
|
const sidebarStatusBreakdown = computed(() => [
|
||||||
{ key: 'done', label: 'Completed', value: todayPlanCounters.value.done, tone: 'done' },
|
{ key: 'done', label: 'Completed', value: issueStatusCounters.value.done, tone: 'done' },
|
||||||
{ key: 'doing', label: 'In Progress', value: todayPlanCounters.value.doing, tone: 'doing' },
|
{ key: 'doing', label: 'In Progress', value: issueStatusCounters.value.doing, tone: 'doing' },
|
||||||
{ key: 'pending', label: 'Pending', value: todayPlanCounters.value.pending, tone: 'pending' },
|
{ key: 'pending', label: 'Pending', value: issueStatusCounters.value.pending, tone: 'pending' },
|
||||||
{ key: 'total', label: 'Total', value: todayPlanCounters.value.total, tone: 'total' },
|
{ key: 'total', label: 'Total', value: issueStatusCounters.value.total, tone: 'total' },
|
||||||
])
|
])
|
||||||
|
|
||||||
// 模拟数据 - 用于测试滑动条
|
const sidebarFocusItems = computed<SidebarFocusItem[]>(() => (
|
||||||
const mockFocusItems: SidebarFocusItem[] = [
|
(todayPlanDetail.value?.focus_tasks ?? []).map((task) => ({
|
||||||
{ id: 'mock-1', label: '任务', title: '完成用户登录模块开发', meta: '处理中', tone: 'doing' },
|
id: task.id,
|
||||||
{ id: 'mock-2', label: '任务', title: '修复首页加载慢的问题', meta: '待启动', tone: 'pending' },
|
label: focusLabelByQuadrant(task.quadrant),
|
||||||
{ id: 'mock-3', label: '目标', title: '本周完成核心功能上线', meta: '今日目标推进', tone: 'doing' },
|
title: task.title,
|
||||||
{ id: 'mock-4', label: '待办', title: '整理本周工作报告', meta: '手动记录', tone: 'done' },
|
meta: focusMeta(task.status, task.dispatch_status, task.assignee_type, task.assignee_id),
|
||||||
{ id: 'mock-5', label: '任务', title: '优化数据库查询性能', meta: '待启动', tone: 'pending' },
|
tone: toneFromStatus(task.dispatch_status !== 'idle' ? task.dispatch_status : task.status),
|
||||||
{ id: 'mock-6', label: '提醒', title: '下午3点团队会议', meta: '15:00', tone: 'pending' },
|
}))
|
||||||
{ id: 'mock-7', label: '任务', title: 'Code Review 代码审查', meta: '处理中', tone: 'doing' },
|
))
|
||||||
{ id: 'mock-8', label: '待办', title: '更新项目文档', meta: '系统同步', tone: 'done' },
|
|
||||||
{ id: 'mock-9', label: '任务', title: '部署测试环境', meta: '待启动', tone: 'pending' },
|
|
||||||
{ id: 'mock-10', label: '目标', title: '本月用户增长10%', meta: '今日目标推进', tone: 'pending' },
|
|
||||||
{ id: 'mock-11', label: '待办', title: '提交本周周报', meta: '手动记录', tone: 'done' },
|
|
||||||
{ id: 'mock-12', label: '任务', title: '接口联调测试', meta: '待启动', tone: 'pending' },
|
|
||||||
{ id: 'mock-13', label: '提醒', title: '周三产品评审会', meta: '14:00', tone: 'pending' },
|
|
||||||
{ id: 'mock-14', label: '任务', title: '性能优化与监控', meta: '处理中', tone: 'doing' },
|
|
||||||
{ id: 'mock-15', label: '待办', title: '备份重要数据', meta: '系统同步', tone: 'done' },
|
|
||||||
{ id: 'mock-16', label: '任务', title: '编写单元测试用例', meta: '待启动', tone: 'pending' },
|
|
||||||
{ id: 'mock-17', label: '目标', title: '提升系统安全性', meta: '今日目标推进', tone: 'pending' },
|
|
||||||
{ id: 'mock-18', label: '待办', title: '清理无用代码文件', meta: '手动记录', tone: 'done' },
|
|
||||||
{ id: 'mock-19', label: '任务', title: '配置CI/CD自动化部署', meta: '待启动', tone: 'pending' },
|
|
||||||
{ id: 'mock-20', label: '提醒', title: '周五项目复盘会', meta: '10:00', tone: 'pending' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const sidebarFocusItems = computed<SidebarFocusItem[]>(() => {
|
|
||||||
// 暂时强制返回模拟数据用于测试
|
|
||||||
return mockFocusItems
|
|
||||||
})
|
|
||||||
|
|
||||||
const sidebarReviewAchievements = computed(() => {
|
const sidebarReviewAchievements = computed(() => {
|
||||||
const stats = monthReviewStats.value
|
const stats = monthReviewStats.value
|
||||||
const items = [
|
const items = [
|
||||||
stats.todoCompleted > 0 ? `累计完成 ${stats.todoCompleted} 项待办,执行节啬已形成闭环。` : '',
|
stats.todoCompleted > 0 ? `Completed ${stats.todoCompleted} todos this month.` : '',
|
||||||
stats.activeDays > 0 ? `本月已有 ${stats.activeDays} 天产生有效计划记录,日程连贯性稳定。` : '',
|
stats.activeDays > 0 ? `${stats.activeDays} active planning days recorded this month.` : '',
|
||||||
stats.highPriorityTotal > 0 ? `高优事项共 ${stats.highPriorityTotal} 项进行中,重点任务没有脱离视野。` : '',
|
stats.highPriorityTotal > 0 ? `${stats.highPriorityTotal} high-priority items were tracked.` : '',
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
if (items.length > 0) return items.slice(0, 3)
|
if (items.length > 0) return items.slice(0, 3)
|
||||||
return ['本月计划数据还在积累中,可以从今日重点开始逐步建立复盘样本。']
|
return ['Monthly review data is still accumulating.']
|
||||||
})
|
})
|
||||||
|
|
||||||
const sidebarReviewReflections = computed(() => {
|
const sidebarReviewReflections = computed(() => {
|
||||||
const stats = monthReviewStats.value
|
const stats = monthReviewStats.value
|
||||||
const pendingTodoCount = Math.max(stats.todoTotal - stats.todoCompleted, 0)
|
const pendingTodoCount = Math.max(stats.todoTotal - stats.todoCompleted, 0)
|
||||||
const items = [
|
const items = [
|
||||||
pendingTodoCount > 0 ? `仍有 ${pendingTodoCount} 项待办未完成,建议拆成更短的收尾窗口。` : '',
|
pendingTodoCount > 0 ? `${pendingTodoCount} todos are still open and may need to be broken down further.` : '',
|
||||||
stats.highPriorityTotal >= 8 ? '高优事项密度偏高,最好提前锁定 1 到 2 个绝对优先级别。' : '',
|
stats.highPriorityTotal >= 8 ? 'High-priority load is dense. Consider narrowing the active mainline.' : '',
|
||||||
stats.reminderTotal >= Math.max(6, stats.activeDays) ? '提醒数量较多,说明执行中断点偏多,适合增加固定回固时段。' : '',
|
stats.reminderTotal >= Math.max(6, stats.activeDays) ? 'Reminder volume is high. A fixed review block may help reduce interruption cost.' : '',
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|
||||||
if (items.length > 0) return items.slice(0, 3)
|
if (items.length > 0) return items.slice(0, 3)
|
||||||
return ['本月节啬相对稳定,下一步可以把重点事项再收到更清晰的主线。']
|
return ['Execution rhythm looks stable.']
|
||||||
})
|
})
|
||||||
|
|
||||||
const sidebarFeedItems = computed<SidebarNewsItem[]>(() => [
|
const sidebarFeedItems = computed<SidebarNewsItem[]>(() => [
|
||||||
{ id: 'fallback-1', title: 'AI 研发节啬继续升温,模型与工作流一体化成为主溜话题。', meta: 'Industry' },
|
{ id: 'fallback-1', title: 'Task orchestration is replacing simple one-shot assistants in modern AI workflows.', meta: 'Industry' },
|
||||||
{ id: 'fallback-2', title: '本地知识库与计划系统的联动体验,正在成为效率工具的新竞争点。', meta: 'Product' },
|
{ id: 'fallback-2', title: 'Shared planning state across chat and execution surfaces is becoming a core product boundary.', meta: 'Product' },
|
||||||
{ id: 'fallback-3', title: '建议接入真实 RSS 源后替换当前占位卡片,以获得即时资讯流。', meta: 'System' },
|
{ id: 'fallback-3', title: 'This feed is still placeholder content and can be replaced by a real RSS source later.', meta: 'System' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const topbarFeedItems = computed(() => sidebarFeedItems.value.length > 0 ? [...sidebarFeedItems.value, ...sidebarFeedItems.value] : [])
|
const topbarFeedItems = computed(() => (
|
||||||
|
sidebarFeedItems.value.length > 0 ? [...sidebarFeedItems.value, ...sidebarFeedItems.value] : []
|
||||||
|
))
|
||||||
|
|
||||||
async function loadSidebarPlanSnapshot(date = new Date()) {
|
async function loadIssueStatusSnapshot() {
|
||||||
|
try {
|
||||||
|
const response = await taskApi.list()
|
||||||
|
issueTasks.value = response.data.filter((task) => task.status !== 'cancelled')
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to load issue status snapshot:', err)
|
||||||
|
issueTasks.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDailyPlanSnapshot(date = new Date()) {
|
||||||
const dateKey = formatDateKey(date)
|
const dateKey = formatDateKey(date)
|
||||||
const monthKey = formatMonthKey(date)
|
const monthKey = formatMonthKey(date)
|
||||||
try {
|
try {
|
||||||
@@ -213,6 +356,13 @@ export function useSidebarPlan(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadSidebarPlanSnapshot(date = new Date()) {
|
||||||
|
await Promise.all([
|
||||||
|
loadDailyPlanSnapshot(date),
|
||||||
|
loadIssueStatusSnapshot(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
watch(todayDateKey, (next, previous) => {
|
watch(todayDateKey, (next, previous) => {
|
||||||
if (next === previous) return
|
if (next === previous) return
|
||||||
void loadDailyDigestFn()
|
void loadDailyDigestFn()
|
||||||
@@ -225,15 +375,46 @@ export function useSidebarPlan(
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadDailyDigestFn()
|
void loadDailyDigestFn()
|
||||||
void loadSidebarPlanSnapshot(new Date())
|
void loadSidebarPlanSnapshot(clientTimeRef.value)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('jarvis:today-status-refresh', handleExternalRefresh)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleExternalRefresh() {
|
||||||
|
void loadSidebarPlanSnapshot(clientTimeRef.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.removeEventListener('jarvis:today-status-refresh', handleExternalRefresh)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
todayPlanDetail, monthPlanDays, todayDateKey, monthPlanSummaryMap,
|
todayPlanDetail,
|
||||||
calendarCells, calendarYear, calendarMonth, todayPlanCounters, monthReviewStats,
|
monthPlanDays,
|
||||||
sidebarWeekLabels, sidebarStatusHeadline, sidebarStatusBreakdown,
|
todayDateKey,
|
||||||
sidebarFocusItems, sidebarReviewAchievements, sidebarReviewReflections,
|
monthPlanSummaryMap,
|
||||||
sidebarFeedItems, topbarFeedItems, loadSidebarPlanSnapshot, sidebarCollapsedModules,
|
calendarCells,
|
||||||
selectedDate, selectCalendarDate, conversationDateMap
|
calendarYear,
|
||||||
|
calendarMonth,
|
||||||
|
issueStatusCounters,
|
||||||
|
monthReviewStats,
|
||||||
|
sidebarWeekLabels,
|
||||||
|
sidebarStatusHeadline,
|
||||||
|
sidebarStatusBreakdown,
|
||||||
|
sidebarFocusItems,
|
||||||
|
issueStatusQuadrants,
|
||||||
|
issueCommanderSummary,
|
||||||
|
sidebarReviewAchievements,
|
||||||
|
sidebarReviewReflections,
|
||||||
|
sidebarFeedItems,
|
||||||
|
topbarFeedItems,
|
||||||
|
loadSidebarPlanSnapshot,
|
||||||
|
sidebarCollapsedModules,
|
||||||
|
selectedDate,
|
||||||
|
selectCalendarDate,
|
||||||
|
conversationDateMap,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, toRef } from 'vue'
|
import { ref, toRef } from 'vue'
|
||||||
import {
|
import {
|
||||||
|
Database,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Send,
|
Send,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
@@ -14,6 +15,7 @@ import FileMessage from '@/components/chat/FileMessage.vue'
|
|||||||
import KnowledgeHudPanel from '@/components/chat/KnowledgeHudPanel.vue'
|
import KnowledgeHudPanel from '@/components/chat/KnowledgeHudPanel.vue'
|
||||||
import KnowledgeSlidePanel from '@/components/chat/KnowledgeSlidePanel.vue'
|
import KnowledgeSlidePanel from '@/components/chat/KnowledgeSlidePanel.vue'
|
||||||
import KnowledgeHUDPreview from '@/components/chat/KnowledgeHUDPreview.vue'
|
import KnowledgeHUDPreview from '@/components/chat/KnowledgeHUDPreview.vue'
|
||||||
|
import KnowledgeRAGPanel from '@/components/chat/KnowledgeRAGPanel.vue'
|
||||||
import OrchestrationPanel from '@/components/chat/OrchestrationPanel.vue'
|
import OrchestrationPanel from '@/components/chat/OrchestrationPanel.vue'
|
||||||
import KanbanPanel from '@/components/chat/KanbanPanel.vue'
|
import KanbanPanel from '@/components/chat/KanbanPanel.vue'
|
||||||
import KanbanDetail from '@/components/chat/KanbanDetail.vue'
|
import KanbanDetail from '@/components/chat/KanbanDetail.vue'
|
||||||
@@ -21,11 +23,14 @@ import TelemetrySparkline from '@/components/chat/TelemetrySparkline.vue'
|
|||||||
import NavShortcutRow from '@/components/navigation/NavShortcutRow.vue'
|
import NavShortcutRow from '@/components/navigation/NavShortcutRow.vue'
|
||||||
import DailyDigestCard from '@/components/memory/DailyDigestCard.vue'
|
import DailyDigestCard from '@/components/memory/DailyDigestCard.vue'
|
||||||
import ReminderToast from '@/components/memory/ReminderToast.vue'
|
import ReminderToast from '@/components/memory/ReminderToast.vue'
|
||||||
|
import type { TaskQuadrant } from '@/api/task'
|
||||||
|
import { documentApi } from '@/api/document'
|
||||||
import { useChatView } from '@/pages/chat/composables/useChatView'
|
import { useChatView } from '@/pages/chat/composables/useChatView'
|
||||||
import { useKnowledgeView } from '@/pages/knowledge/composables/useKnowledgeView'
|
import { useKnowledgeView } from '@/pages/knowledge/composables/useKnowledgeView'
|
||||||
import { useDailyDigest } from '@/pages/chat/composables/useDailyDigest'
|
import { useDailyDigest } from '@/pages/chat/composables/useDailyDigest'
|
||||||
import { useClientTime, formatNetworkRate } from '@/pages/chat/composables/useClientTime'
|
import { useClientTime, formatNetworkRate } from '@/pages/chat/composables/useClientTime'
|
||||||
import { useSidebarPlan, formatDateKey } from '@/pages/chat/composables/useSidebarPlan'
|
import { useSidebarPlan, formatDateKey } from '@/pages/chat/composables/useSidebarPlan'
|
||||||
|
import TempleModal from '@/pages/temple/index.vue'
|
||||||
|
|
||||||
// --- Chat view (core messaging logic) ---
|
// --- Chat view (core messaging logic) ---
|
||||||
const {
|
const {
|
||||||
@@ -37,10 +42,9 @@ const {
|
|||||||
isTyping,
|
isTyping,
|
||||||
fileInputRef,
|
fileInputRef,
|
||||||
showEmojiPicker,
|
showEmojiPicker,
|
||||||
chatModels,
|
|
||||||
selectedModelName,
|
selectedModelName,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
isLoadingModels,
|
selectedRuntime,
|
||||||
orchestrationStatus,
|
orchestrationStatus,
|
||||||
orchestrationInsight,
|
orchestrationInsight,
|
||||||
activeAgent,
|
activeAgent,
|
||||||
@@ -67,18 +71,18 @@ const {
|
|||||||
} = useKnowledgeView()
|
} = useKnowledgeView()
|
||||||
|
|
||||||
// --- Client time & weather ---
|
// --- Client time & weather ---
|
||||||
const { clientTime, city, weatherIcon, weatherSummary, formatClientDate, formatClientClock } = useClientTime()
|
const { clientTime, city, weatherIcon, weatherSummary } = useClientTime()
|
||||||
|
|
||||||
// --- Daily digest & reminders ---
|
// --- Daily digest & reminders ---
|
||||||
const { dailyDigest, digestLoading, activeReminder, reminderVisible, loadDailyDigest, handleSnooze, handleDismiss } = useDailyDigest()
|
const { dailyDigest, digestLoading, activeReminder, reminderVisible, loadDailyDigest, handleSnooze, handleDismiss } = useDailyDigest()
|
||||||
|
|
||||||
// --- Sidebar plan (calendar, focus, review) ---
|
// --- Sidebar plan (calendar, focus, review) ---
|
||||||
const {
|
const {
|
||||||
calendarCells, calendarYear, calendarMonth, todayPlanCounters,
|
calendarCells, issueStatusCounters,
|
||||||
sidebarWeekLabels, sidebarStatusHeadline, sidebarStatusBreakdown,
|
sidebarWeekLabels, sidebarStatusHeadline, sidebarStatusBreakdown,
|
||||||
sidebarFocusItems, sidebarReviewAchievements, sidebarReviewReflections,
|
sidebarFocusItems, issueStatusQuadrants, issueCommanderSummary, sidebarReviewAchievements, sidebarReviewReflections,
|
||||||
sidebarFeedItems, topbarFeedItems, sidebarCollapsedModules,
|
sidebarFeedItems, topbarFeedItems, sidebarCollapsedModules,
|
||||||
selectedDate, selectCalendarDate
|
selectCalendarDate, loadSidebarPlanSnapshot
|
||||||
} = useSidebarPlan(clientTime, loadDailyDigest, toRef(store, 'conversations'))
|
} = useSidebarPlan(clientTime, loadDailyDigest, toRef(store, 'conversations'))
|
||||||
|
|
||||||
// --- Local UI state ---
|
// --- Local UI state ---
|
||||||
@@ -86,32 +90,93 @@ const sidebarCollapsed = ref(false)
|
|||||||
const orchestrationDrawerOpen = ref(false)
|
const orchestrationDrawerOpen = ref(false)
|
||||||
const kanbanDrawerOpen = ref(false)
|
const kanbanDrawerOpen = ref(false)
|
||||||
const kanbanDetailOpen = ref(false)
|
const kanbanDetailOpen = ref(false)
|
||||||
const kanbanDetailQuadrant = ref<{ id: string; title: string; color: string } | null>(null)
|
const kanbanDetailState = ref<{ mode: 'create' | 'edit'; taskId?: string | null; quadrant?: TaskQuadrant | null } | null>(null)
|
||||||
const knowledgeHudOpen = ref(false)
|
const knowledgeHudOpen = ref(false)
|
||||||
|
const knowledgeRAGOpen = ref(false)
|
||||||
|
const ragPanelRef = ref<any>(null)
|
||||||
const selectedFolder = ref<any>(null)
|
const selectedFolder = ref<any>(null)
|
||||||
const previewDoc = ref<any>(null)
|
const previewDoc = ref<any>(null)
|
||||||
|
const templeVisible = ref(false)
|
||||||
|
|
||||||
function openOrchestrationDrawer() { orchestrationDrawerOpen.value = true }
|
function openOrchestrationDrawer() { orchestrationDrawerOpen.value = true }
|
||||||
function closeOrchestrationDrawer() { orchestrationDrawerOpen.value = false }
|
function closeOrchestrationDrawer() { orchestrationDrawerOpen.value = false }
|
||||||
function openKanbanDrawer() { kanbanDrawerOpen.value = true }
|
function openKanbanDrawer() { kanbanDrawerOpen.value = true }
|
||||||
function closeKanbanDrawer() { kanbanDrawerOpen.value = false }
|
function closeKanbanDrawer() { kanbanDrawerOpen.value = false }
|
||||||
function openKanbanDetail(quadrantId: string) {
|
function openKanbanCreate(quadrantId: string) {
|
||||||
const quadrantMap: Record<string, { title: string; color: string }> = {
|
kanbanDetailState.value = { mode: 'create', quadrant: quadrantId as TaskQuadrant }
|
||||||
'urgent-important': { title: '重要且紧急', color: '#f56565' },
|
|
||||||
'not-urgent-important': { title: '重要不紧急', color: '#ecc94b' },
|
|
||||||
'urgent-not-important': { title: '紧急不重要', color: '#42b9f5' },
|
|
||||||
'not-urgent-not-important': { title: '不重要不紧急', color: '#97c950' },
|
|
||||||
}
|
|
||||||
kanbanDetailQuadrant.value = { id: quadrantId, ...quadrantMap[quadrantId] }
|
|
||||||
kanbanDetailOpen.value = true
|
kanbanDetailOpen.value = true
|
||||||
}
|
}
|
||||||
function closeKanbanDetail() { kanbanDetailOpen.value = false }
|
function openKanbanTask(taskId: string) {
|
||||||
|
kanbanDetailState.value = { mode: 'edit', taskId }
|
||||||
|
kanbanDetailOpen.value = true
|
||||||
|
}
|
||||||
|
function closeKanbanDetail() {
|
||||||
|
kanbanDetailOpen.value = false
|
||||||
|
kanbanDetailState.value = null
|
||||||
|
refreshTodayStatus()
|
||||||
|
}
|
||||||
|
function refreshTodayStatus() {
|
||||||
|
void loadSidebarPlanSnapshot(clientTime.value)
|
||||||
|
}
|
||||||
|
async function handleKanbanSaved() {
|
||||||
|
await loadSidebarPlanSnapshot(clientTime.value)
|
||||||
|
}
|
||||||
|
async function handleKanbanDeleted() {
|
||||||
|
await loadSidebarPlanSnapshot(clientTime.value)
|
||||||
|
closeKanbanDetail()
|
||||||
|
}
|
||||||
function openKnowledgeHud() {
|
function openKnowledgeHud() {
|
||||||
selectedFolder.value = null
|
selectedFolder.value = null
|
||||||
previewDoc.value = null
|
previewDoc.value = null
|
||||||
knowledgeHudOpen.value = true
|
knowledgeHudOpen.value = true
|
||||||
}
|
}
|
||||||
function closeKnowledgeHud() { knowledgeHudOpen.value = false }
|
function closeKnowledgeHud() { knowledgeHudOpen.value = false }
|
||||||
|
function openKnowledgeRAG() { knowledgeRAGOpen.value = true }
|
||||||
|
function closeKnowledgeRAG() {
|
||||||
|
knowledgeRAGOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAG chat mode - send message to knowledge base instead of chat
|
||||||
|
async function sendRAGMessage() {
|
||||||
|
if (!inputMessage.value.trim() || isSending.value) return
|
||||||
|
|
||||||
|
const userQuery = inputMessage.value.trim()
|
||||||
|
inputMessage.value = ''
|
||||||
|
isSending.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Add user message to RAG panel
|
||||||
|
ragPanelRef.value?.addMessage({
|
||||||
|
id: Date.now().toString(),
|
||||||
|
role: 'user',
|
||||||
|
content: userQuery,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Call RAG API
|
||||||
|
const response = await documentApi.ragChat({
|
||||||
|
query: userQuery,
|
||||||
|
top_k: 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add assistant response to RAG panel
|
||||||
|
ragPanelRef.value?.addMessage({
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: response.data.answer,
|
||||||
|
sources: response.data.sources || [],
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback response
|
||||||
|
ragPanelRef.value?.addMessage({
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: '抱歉,搜索知识库时出现问题。请稍后重试。',
|
||||||
|
sources: [],
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
isSending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
function handleSelectFolder(folder: any) { selectedFolder.value = folder }
|
function handleSelectFolder(folder: any) { selectedFolder.value = folder }
|
||||||
function handleOpenPreview(doc: any) { previewDoc.value = doc }
|
function handleOpenPreview(doc: any) { previewDoc.value = doc }
|
||||||
|
|
||||||
@@ -291,7 +356,7 @@ function renderMarkdown(content: string) {
|
|||||||
v-for="cell in calendarCells"
|
v-for="cell in calendarCells"
|
||||||
:key="cell.key"
|
:key="cell.key"
|
||||||
class="calendar-day"
|
class="calendar-day"
|
||||||
:class="{ active: cell.active, busy: cell.busy, muted: cell.value === null, selected: cell.selected, clickable: cell.active || cell.hasConversation }"
|
:class="{ active: cell.active, muted: cell.value === null, selected: cell.selected, clickable: cell.active || cell.hasConversation }"
|
||||||
@click="(cell.active || cell.hasConversation) && handleCalendarDateSelect(cell.key)"
|
@click="(cell.active || cell.hasConversation) && handleCalendarDateSelect(cell.key)"
|
||||||
>
|
>
|
||||||
{{ cell.value ?? '' }}
|
{{ cell.value ?? '' }}
|
||||||
@@ -303,11 +368,11 @@ function renderMarkdown(content: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="jarvis-panel jarvis-plan-panel" @click="openKanbanDrawer" style="cursor: pointer;">
|
<div class="jarvis-panel jarvis-plan-panel" @click="openKanbanDrawer" style="cursor: pointer;">
|
||||||
<div class="jarvis-section-title">TODAY'S STATUS</div>
|
<div class="jarvis-section-title">ISSUE STATUS</div>
|
||||||
<div class="jarvis-status-shell">
|
<div class="jarvis-status-shell">
|
||||||
<div class="jarvis-progress-ring" :style="{ '--completion': `${todayPlanCounters.completion}%` }">
|
<div class="jarvis-progress-ring" :style="{ '--completion': `${issueStatusCounters.completion}%` }">
|
||||||
<div class="jarvis-progress-core">
|
<div class="jarvis-progress-core">
|
||||||
<strong>{{ todayPlanCounters.completion }}%</strong>
|
<strong>{{ issueStatusCounters.completion }}%</strong>
|
||||||
<span>COMPLETION</span>
|
<span>COMPLETION</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,6 +451,16 @@ function renderMarkdown(content: string) {
|
|||||||
<span>{{ selectedModel?.model || selectedModelName || 'Default' }}</span>
|
<span>{{ selectedModel?.model || selectedModelName || 'Default' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="chat-model-panel">
|
||||||
|
<label class="chat-model-label" for="chat-runtime-select">
|
||||||
|
<Sparkles :size="12" />
|
||||||
|
<span>RUNTIME</span>
|
||||||
|
</label>
|
||||||
|
<select id="chat-runtime-select" v-model="selectedRuntime" class="chat-runtime-select">
|
||||||
|
<option value="jarvis">Jarvis</option>
|
||||||
|
<option value="hermes">Hermes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chat-intel-strip" v-if="sidebarFeedItems.length > 0">
|
<div class="chat-intel-strip" v-if="sidebarFeedItems.length > 0">
|
||||||
@@ -513,10 +588,10 @@ function renderMarkdown(content: string) {
|
|||||||
|
|
||||||
<!-- Top buttons above input -->
|
<!-- Top buttons above input -->
|
||||||
<div class="top-buttons-row">
|
<div class="top-buttons-row">
|
||||||
<button class="top-action-btn" @click="$router.push('/temple')" title="Temple">
|
<button class="top-action-btn" @click="templeVisible = true" title="Temple">
|
||||||
<span class="btn-icon temple-icon">◈</span>
|
<span class="btn-icon temple-icon">◈</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="top-action-btn" @click="$router.push('/knowledge')" title="Knowledge">
|
<button class="top-action-btn" @click="openKnowledgeRAG()" title="Knowledge">
|
||||||
<span class="btn-icon knowledge-icon">◉</span>
|
<span class="btn-icon knowledge-icon">◉</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="top-action-btn" @click="$router.push('/war-room')" title="War Room">
|
<button class="top-action-btn" @click="$router.push('/war-room')" title="War Room">
|
||||||
@@ -534,10 +609,10 @@ function renderMarkdown(content: string) {
|
|||||||
<textarea
|
<textarea
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
v-model="inputMessage"
|
v-model="inputMessage"
|
||||||
placeholder="输入指令,按 Enter 发送..."
|
:placeholder="knowledgeRAGOpen ? '输入问题搜索知识库...' : '输入指令,按 Enter 发送...'"
|
||||||
:disabled="isSending"
|
:disabled="isSending"
|
||||||
rows="1"
|
rows="1"
|
||||||
@keydown.enter.exact.prevent="sendMessage"
|
@keydown.enter.exact.prevent="knowledgeRAGOpen ? sendRAGMessage() : sendMessage()"
|
||||||
@input="autoResize"
|
@input="autoResize"
|
||||||
></textarea>
|
></textarea>
|
||||||
<input
|
<input
|
||||||
@@ -570,16 +645,21 @@ function renderMarkdown(content: string) {
|
|||||||
class="send-btn"
|
class="send-btn"
|
||||||
:class="{ active: inputMessage.trim() }"
|
:class="{ active: inputMessage.trim() }"
|
||||||
:disabled="!inputMessage.trim() || isSending"
|
:disabled="!inputMessage.trim() || isSending"
|
||||||
@click="sendMessage"
|
@click="knowledgeRAGOpen ? sendRAGMessage() : sendMessage()"
|
||||||
>
|
>
|
||||||
<Send :size="15" />
|
<Send :size="15" />
|
||||||
<CornerDownLeft :size="12" class="enter-hint" />
|
<CornerDownLeft :size="12" class="enter-hint" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-hints">
|
<div class="input-hints">
|
||||||
<span class="hint-item">ENTER 发送</span>
|
<template v-if="knowledgeRAGOpen">
|
||||||
<span class="hint-sep">|</span>
|
<span class="hint-item rag-hint">知识库搜索模式</span>
|
||||||
<span class="hint-item">SHIFT+ENTER 换行</span>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="hint-item">ENTER 发送</span>
|
||||||
|
<span class="hint-sep">|</span>
|
||||||
|
<span class="hint-item">SHIFT+ENTER 换行</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -719,6 +799,15 @@ function renderMarkdown(content: string) {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Knowledge RAG Panel (Slide-up from bottom) -->
|
||||||
|
<KnowledgeRAGPanel
|
||||||
|
v-if="knowledgeRAGOpen"
|
||||||
|
ref="ragPanelRef"
|
||||||
|
:is-chat-loading="isSending"
|
||||||
|
@close="closeKnowledgeRAG"
|
||||||
|
@send="sendRAGMessage"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="agent-drawer-shell" :class="{ open: orchestrationDrawerOpen }">
|
<div class="agent-drawer-shell" :class="{ open: orchestrationDrawerOpen }">
|
||||||
<button
|
<button
|
||||||
v-if="orchestrationDrawerOpen"
|
v-if="orchestrationDrawerOpen"
|
||||||
@@ -757,8 +846,11 @@ function renderMarkdown(content: string) {
|
|||||||
<aside class="kanban-drawer" :class="{ open: kanbanDrawerOpen }">
|
<aside class="kanban-drawer" :class="{ open: kanbanDrawerOpen }">
|
||||||
<KanbanPanel
|
<KanbanPanel
|
||||||
:visible="kanbanDrawerOpen"
|
:visible="kanbanDrawerOpen"
|
||||||
|
:quadrants="issueStatusQuadrants"
|
||||||
|
:commander-summary="issueCommanderSummary"
|
||||||
@close="closeKanbanDrawer"
|
@close="closeKanbanDrawer"
|
||||||
@open-detail="openKanbanDetail"
|
@create-task="openKanbanCreate"
|
||||||
|
@open-task="openKanbanTask"
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@@ -766,12 +858,14 @@ function renderMarkdown(content: string) {
|
|||||||
<!-- Kanban Detail Modal (Teleported to body to avoid blur) -->
|
<!-- Kanban Detail Modal (Teleported to body to avoid blur) -->
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<KanbanDetail
|
<KanbanDetail
|
||||||
v-if="kanbanDetailQuadrant"
|
v-if="kanbanDetailState"
|
||||||
:visible="kanbanDetailOpen"
|
:visible="kanbanDetailOpen"
|
||||||
:quadrant-id="kanbanDetailQuadrant.id"
|
:mode="kanbanDetailState.mode"
|
||||||
:quadrant-title="kanbanDetailQuadrant.title"
|
:task-id="kanbanDetailState.taskId"
|
||||||
:quadrant-color="kanbanDetailQuadrant.color"
|
:default-quadrant="kanbanDetailState.quadrant"
|
||||||
@close="closeKanbanDetail"
|
@close="closeKanbanDetail"
|
||||||
|
@saved="handleKanbanSaved"
|
||||||
|
@deleted="handleKanbanDeleted"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
@@ -807,6 +901,12 @@ function renderMarkdown(content: string) {
|
|||||||
@dismiss="handleDismiss"
|
@dismiss="handleDismiss"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Temple Modal (智慧神殿) -->
|
||||||
|
<TempleModal
|
||||||
|
:visible="templeVisible"
|
||||||
|
@close="templeVisible = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-if="showNewFolderDialog" class="knowledge-hud-preview" @click.self="showNewFolderDialog = false">
|
<div v-if="showNewFolderDialog" class="knowledge-hud-preview" @click.self="showNewFolderDialog = false">
|
||||||
<div class="hud-dialog-jarvis">
|
<div class="hud-dialog-jarvis">
|
||||||
<div class="dialog-header-tech">
|
<div class="dialog-header-tech">
|
||||||
|
|||||||
Reference in New Issue
Block a user