refactor: migrate HomeView to ChatPanel widget, delete DashboardChatInput
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,413 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, nextTick } from "vue";
|
|
||||||
import { apiGet, transcribeAudio } from "@/api/client";
|
|
||||||
import { useVoiceRecorder } from "@/composables/useVoiceRecorder";
|
|
||||||
import { useChatStore } from "@/stores/chat";
|
|
||||||
import { useSettingsStore } from "@/stores/settings";
|
|
||||||
import type { Note } from "@/types/note";
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
submit: [payload: { content: string; contextNoteId?: number }];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const store = useChatStore();
|
|
||||||
|
|
||||||
const messageInput = ref("");
|
|
||||||
const inputEl = ref<HTMLTextAreaElement | null>(null);
|
|
||||||
|
|
||||||
// Note picker state
|
|
||||||
const attachedNote = ref<{ id: number; title: string } | null>(null);
|
|
||||||
const showNotePicker = ref(false);
|
|
||||||
const noteSearchQuery = ref("");
|
|
||||||
const noteSearchResults = ref<{ id: number; title: string }[]>([]);
|
|
||||||
const noteSearchLoading = ref(false);
|
|
||||||
let noteSearchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
function onSubmit() {
|
|
||||||
const content = messageInput.value.trim();
|
|
||||||
if (!content) return;
|
|
||||||
emit("submit", {
|
|
||||||
content,
|
|
||||||
contextNoteId: attachedNote.value?.id,
|
|
||||||
});
|
|
||||||
messageInput.value = "";
|
|
||||||
attachedNote.value = null;
|
|
||||||
resetTextareaHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onInputKeydown(e: KeyboardEvent) {
|
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
onSubmit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function autoResize() {
|
|
||||||
const el = inputEl.value;
|
|
||||||
if (!el) return;
|
|
||||||
el.style.height = "auto";
|
|
||||||
el.style.height = Math.min(el.scrollHeight, 120) + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetTextareaHeight() {
|
|
||||||
const el = inputEl.value;
|
|
||||||
if (!el) return;
|
|
||||||
el.style.height = "auto";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note picker
|
|
||||||
function toggleNotePicker() {
|
|
||||||
showNotePicker.value = !showNotePicker.value;
|
|
||||||
if (showNotePicker.value) {
|
|
||||||
noteSearchQuery.value = "";
|
|
||||||
noteSearchResults.value = [];
|
|
||||||
nextTick(() => {
|
|
||||||
const el = document.querySelector(
|
|
||||||
".dashboard-chat .note-picker-search"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
el?.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onNoteSearchInput() {
|
|
||||||
if (noteSearchTimer) clearTimeout(noteSearchTimer);
|
|
||||||
noteSearchTimer = setTimeout(async () => {
|
|
||||||
const q = noteSearchQuery.value.trim();
|
|
||||||
if (!q) {
|
|
||||||
noteSearchResults.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
noteSearchLoading.value = true;
|
|
||||||
try {
|
|
||||||
const data = await apiGet<{ notes: Note[] }>(
|
|
||||||
`/api/notes?q=${encodeURIComponent(q)}&all=true&limit=5`
|
|
||||||
);
|
|
||||||
noteSearchResults.value = data.notes.map((n) => ({
|
|
||||||
id: n.id,
|
|
||||||
title: n.title,
|
|
||||||
}));
|
|
||||||
} catch {
|
|
||||||
noteSearchResults.value = [];
|
|
||||||
} finally {
|
|
||||||
noteSearchLoading.value = false;
|
|
||||||
}
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectNote(note: { id: number; title: string }) {
|
|
||||||
attachedNote.value = note;
|
|
||||||
showNotePicker.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeAttachedNote() {
|
|
||||||
attachedNote.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function focus() {
|
|
||||||
inputEl.value?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Voice PTT
|
|
||||||
const settingsStore = useSettingsStore();
|
|
||||||
const voiceEnabled = computed(() => settingsStore.voiceSttReady);
|
|
||||||
const transcribingVoice = ref(false);
|
|
||||||
const recorder = useVoiceRecorder();
|
|
||||||
|
|
||||||
async function startPtt() {
|
|
||||||
if (!voiceEnabled.value || recorder.recording.value) return;
|
|
||||||
await recorder.startRecording();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stopPtt() {
|
|
||||||
if (!recorder.recording.value) return;
|
|
||||||
transcribingVoice.value = true;
|
|
||||||
try {
|
|
||||||
const blob = await recorder.stopRecording();
|
|
||||||
const { transcript } = await transcribeAudio(blob);
|
|
||||||
if (transcript.trim()) {
|
|
||||||
messageInput.value = (messageInput.value ? messageInput.value + " " : "") + transcript.trim();
|
|
||||||
await nextTick();
|
|
||||||
autoResize();
|
|
||||||
}
|
|
||||||
} catch { /* transcription failed silently */ } finally {
|
|
||||||
transcribingVoice.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ focus });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="dashboard-chat">
|
|
||||||
<!-- Attached note pill -->
|
|
||||||
<div v-if="attachedNote" class="attached-note">
|
|
||||||
<span class="attached-note-pill">
|
|
||||||
{{ attachedNote.title }}
|
|
||||||
<button class="attached-note-remove" aria-label="Remove attached note" @click="removeAttachedNote">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="chat-input-bar">
|
|
||||||
<div class="note-picker-wrapper">
|
|
||||||
<button
|
|
||||||
class="btn-attach"
|
|
||||||
@click="toggleNotePicker"
|
|
||||||
:disabled="!store.chatReady"
|
|
||||||
title="Attach a note"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div v-if="showNotePicker" class="note-picker-dropdown">
|
|
||||||
<input
|
|
||||||
class="note-picker-search"
|
|
||||||
v-model="noteSearchQuery"
|
|
||||||
@input="onNoteSearchInput"
|
|
||||||
placeholder="Search notes..."
|
|
||||||
/>
|
|
||||||
<div class="note-picker-results">
|
|
||||||
<div
|
|
||||||
v-for="note in noteSearchResults"
|
|
||||||
:key="note.id"
|
|
||||||
class="note-picker-item"
|
|
||||||
@click="selectNote(note)"
|
|
||||||
>
|
|
||||||
{{ note.title || "Untitled" }}
|
|
||||||
</div>
|
|
||||||
<div v-if="noteSearchLoading" class="note-picker-empty">
|
|
||||||
Searching...
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="noteSearchQuery && !noteSearchResults.length"
|
|
||||||
class="note-picker-empty"
|
|
||||||
>
|
|
||||||
No notes found
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
ref="inputEl"
|
|
||||||
v-model="messageInput"
|
|
||||||
@keydown="onInputKeydown"
|
|
||||||
@input="autoResize"
|
|
||||||
:placeholder="
|
|
||||||
!store.chatReady ? 'Chat unavailable'
|
|
||||||
: store.streaming ? 'Type to queue… (Enter to queue)'
|
|
||||||
: 'Start a new chat… (Enter to send)'
|
|
||||||
"
|
|
||||||
:disabled="!store.chatReady"
|
|
||||||
rows="1"
|
|
||||||
></textarea>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="voiceEnabled && recorder.isSupported"
|
|
||||||
class="btn-attach btn-mic-ptt"
|
|
||||||
:class="{ 'mic-recording': recorder.recording.value, 'mic-transcribing': transcribingVoice }"
|
|
||||||
@mousedown.prevent="startPtt"
|
|
||||||
@mouseup.prevent="stopPtt"
|
|
||||||
@touchstart.prevent="startPtt"
|
|
||||||
@touchend.prevent="stopPtt"
|
|
||||||
:disabled="transcribingVoice || !store.chatReady"
|
|
||||||
:title="recorder.recording.value ? 'Release to send' : 'Hold to speak'"
|
|
||||||
aria-label="Push to talk"
|
|
||||||
>
|
|
||||||
<svg v-if="!transcribingVoice" width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
|
|
||||||
</svg>
|
|
||||||
<svg v-else width="17" height="17" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<circle cx="12" cy="12" r="8" opacity="0.35"/>
|
|
||||||
<circle cx="12" cy="12" r="4"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn-send"
|
|
||||||
@click="onSubmit"
|
|
||||||
:disabled="!messageInput.trim() || !store.chatReady"
|
|
||||||
>
|
|
||||||
↑
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.dashboard-chat {
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attached-note {
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
}
|
|
||||||
.attached-note-pill {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 0.2rem 0.5rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
.attached-note-remove {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1;
|
|
||||||
padding: 0 0.15rem;
|
|
||||||
}
|
|
||||||
.attached-note-remove:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.5rem 0.5rem 0.5rem 0.75rem;
|
|
||||||
background: var(--color-input-bar-bg);
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: 0 2px 12px var(--color-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input-bar textarea {
|
|
||||||
flex: 1;
|
|
||||||
resize: none;
|
|
||||||
padding: 0.4rem 0.5rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-input-bar-text);
|
|
||||||
outline: none;
|
|
||||||
max-height: 120px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.chat-input-bar textarea::placeholder {
|
|
||||||
color: var(--color-input-bar-placeholder);
|
|
||||||
}
|
|
||||||
.chat-input-bar textarea:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Note picker */
|
|
||||||
.note-picker-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.btn-attach {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--color-input-bar-text);
|
|
||||||
opacity: 0.6;
|
|
||||||
padding: 0.25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.btn-attach:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.btn-attach:disabled {
|
|
||||||
opacity: 0.3;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.note-picker-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
bottom: calc(100% + 8px);
|
|
||||||
left: 0;
|
|
||||||
width: 280px;
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
box-shadow: 0 4px 16px var(--color-shadow);
|
|
||||||
z-index: 10;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.note-picker-search {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-text);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
outline: none;
|
|
||||||
font-family: inherit;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.note-picker-results {
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.note-picker-item {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.note-picker-item:hover {
|
|
||||||
background: var(--color-bg-secondary);
|
|
||||||
}
|
|
||||||
.note-picker-empty {
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-send {
|
|
||||||
width: 34px;
|
|
||||||
min-width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--color-primary);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.btn-send:disabled {
|
|
||||||
opacity: 0.35;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-mic-ptt {
|
|
||||||
transition: color 0.15s, opacity 0.15s;
|
|
||||||
}
|
|
||||||
.btn-mic-ptt.mic-recording {
|
|
||||||
color: #ef4444;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.btn-mic-ptt.mic-transcribing {
|
|
||||||
color: var(--color-primary);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
import { apiGet, listEvents } from "@/api/client";
|
import { apiGet, listEvents } from "@/api/client";
|
||||||
import { useBackgroundRefresh } from "@/composables/useBackgroundRefresh";
|
import { useBackgroundRefresh } from "@/composables/useBackgroundRefresh";
|
||||||
import { milestoneColor } from "@/utils/palette";
|
import { milestoneColor } from "@/utils/palette";
|
||||||
import { fmtRelativeDateTime } from "@/utils/dateFormat";
|
import { fmtRelativeDateTime } from "@/utils/dateFormat";
|
||||||
import type { Note } from "@/types/note";
|
import type { Note } from "@/types/note";
|
||||||
import type { Task, TaskListResponse, TaskStatus } from "@/types/task";
|
import type { Task, TaskListResponse, TaskStatus } from "@/types/task";
|
||||||
import type { ToolCallRecord, Message } from "@/types/chat";
|
|
||||||
import type { EventEntry } from "@/api/client";
|
import type { EventEntry } from "@/api/client";
|
||||||
import NoteCard from "@/components/NoteCard.vue";
|
import NoteCard from "@/components/NoteCard.vue";
|
||||||
import TaskCard from "@/components/TaskCard.vue";
|
import TaskCard from "@/components/TaskCard.vue";
|
||||||
import StatusBadge from "@/components/StatusBadge.vue";
|
import StatusBadge from "@/components/StatusBadge.vue";
|
||||||
import PriorityBadge from "@/components/PriorityBadge.vue";
|
import PriorityBadge from "@/components/PriorityBadge.vue";
|
||||||
import ToolCallCard from "@/components/ToolCallCard.vue";
|
import ChatPanel from "@/components/ChatPanel.vue";
|
||||||
import DashboardChatInput from "@/components/DashboardChatInput.vue";
|
|
||||||
import EventSlideOver from "@/components/EventSlideOver.vue";
|
import EventSlideOver from "@/components/EventSlideOver.vue";
|
||||||
import { useTasksStore } from "@/stores/tasks";
|
import { useTasksStore } from "@/stores/tasks";
|
||||||
import { useChatStore } from "@/stores/chat";
|
import { useChatStore } from "@/stores/chat";
|
||||||
@@ -157,7 +155,7 @@ onMounted(async () => {
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|
||||||
// Focus chat input after data loads
|
// Focus chat input after data loads
|
||||||
chatInputRef.value?.focus();
|
chatPanelRef.value?.focus();
|
||||||
loadProjects();
|
loadProjects();
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -216,10 +214,10 @@ function onStatusToggle(id: number, status: TaskStatus) {
|
|||||||
|
|
||||||
// ─── Chat widget ──────────────────────────────────────────────────────────────
|
// ─── Chat widget ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const chatInputRef = ref<{ focus: () => void } | null>(null);
|
const chatPanelRef = ref<InstanceType<typeof ChatPanel> | null>(null);
|
||||||
|
|
||||||
function onFocusChatShortcut() {
|
function onFocusChatShortcut() {
|
||||||
chatInputRef.value?.focus();
|
chatPanelRef.value?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -235,16 +233,6 @@ chatStore.fetchStatus().then(() => {
|
|||||||
if (chatStore.defaultModel) chatStore.warmModel(chatStore.defaultModel);
|
if (chatStore.defaultModel) chatStore.warmModel(chatStore.defaultModel);
|
||||||
});
|
});
|
||||||
|
|
||||||
const dashboardConvId = ref<number | null>(null);
|
|
||||||
const dashboardDone = ref(false);
|
|
||||||
const dashboardQuery = ref("");
|
|
||||||
const dashboardFinalContent = ref("");
|
|
||||||
const dashboardFinalToolCalls = ref<ToolCallRecord[]>([]);
|
|
||||||
|
|
||||||
const isConversational = computed(
|
|
||||||
() => dashboardDone.value && dashboardFinalToolCalls.value.length === 0
|
|
||||||
);
|
|
||||||
|
|
||||||
const QUICK_ACTIONS = [
|
const QUICK_ACTIONS = [
|
||||||
"What's due today?",
|
"What's due today?",
|
||||||
"Events this week?",
|
"Events this week?",
|
||||||
@@ -252,38 +240,8 @@ const QUICK_ACTIONS = [
|
|||||||
"My high priority tasks?",
|
"My high priority tasks?",
|
||||||
];
|
];
|
||||||
|
|
||||||
async function onChatSubmit(payload: { content: string; contextNoteId?: number }) {
|
|
||||||
dashboardConvId.value = null;
|
|
||||||
dashboardDone.value = false;
|
|
||||||
dashboardFinalContent.value = "";
|
|
||||||
dashboardFinalToolCalls.value = [];
|
|
||||||
dashboardQuery.value = payload.content;
|
|
||||||
|
|
||||||
const conv = await chatStore.createConversation();
|
|
||||||
await chatStore.fetchConversation(conv.id);
|
|
||||||
dashboardConvId.value = conv.id;
|
|
||||||
|
|
||||||
await chatStore.sendMessage(payload.content, payload.contextNoteId);
|
|
||||||
|
|
||||||
const msgs = chatStore.currentConversation?.messages ?? [];
|
|
||||||
const lastAssistant = [...msgs]
|
|
||||||
.reverse()
|
|
||||||
.find((m: Message) => m.role === "assistant");
|
|
||||||
dashboardFinalContent.value = lastAssistant?.content ?? "";
|
|
||||||
dashboardFinalToolCalls.value = (lastAssistant?.tool_calls ?? []) as ToolCallRecord[];
|
|
||||||
dashboardDone.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onQuickAction(query: string) {
|
async function onQuickAction(query: string) {
|
||||||
await onChatSubmit({ content: query });
|
await chatPanelRef.value?.send(query);
|
||||||
}
|
|
||||||
|
|
||||||
function clearDashboardResponse() {
|
|
||||||
dashboardConvId.value = null;
|
|
||||||
dashboardDone.value = false;
|
|
||||||
dashboardQuery.value = "";
|
|
||||||
dashboardFinalContent.value = "";
|
|
||||||
dashboardFinalToolCalls.value = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Upcoming events slide-over ───────────────────────────────────────────────
|
// ─── Upcoming events slide-over ───────────────────────────────────────────────
|
||||||
@@ -324,38 +282,9 @@ function formatUpcomingTime(event: EventEntry): string {
|
|||||||
@click="onQuickAction(q)"
|
@click="onQuickAction(q)"
|
||||||
>{{ q }}</button>
|
>{{ q }}</button>
|
||||||
</div>
|
</div>
|
||||||
<DashboardChatInput ref="chatInputRef" @submit="onChatSubmit" />
|
<ChatPanel ref="chatPanelRef" variant="widget" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Inline response ────────────────────────────────────── -->
|
|
||||||
<div v-if="dashboardConvId" class="dashboard-response">
|
|
||||||
<div class="dashboard-response-query">{{ dashboardQuery }}</div>
|
|
||||||
<div v-if="chatStore.streaming && chatStore.streamingToolCalls.length" class="dashboard-tool-calls">
|
|
||||||
<ToolCallCard v-for="(tc, i) in chatStore.streamingToolCalls" :key="i" :tool-call="tc" />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="dashboardDone && dashboardFinalToolCalls.length" class="dashboard-tool-calls">
|
|
||||||
<ToolCallCard v-for="(tc, i) in dashboardFinalToolCalls" :key="i" :tool-call="tc" />
|
|
||||||
</div>
|
|
||||||
<div v-if="chatStore.streaming" class="dashboard-response-text streaming">
|
|
||||||
<div v-if="chatStore.streamingStatus && !chatStore.streamingContent" class="dashboard-status-line">
|
|
||||||
<span class="dashboard-status-dot"></span>{{ chatStore.streamingStatus }}
|
|
||||||
</div>
|
|
||||||
<span v-else-if="chatStore.streamingContent">{{ chatStore.streamingContent }}</span>
|
|
||||||
<span v-else class="thinking-dots">...</span>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="dashboardDone && dashboardFinalContent" class="dashboard-response-text">
|
|
||||||
{{ dashboardFinalContent }}
|
|
||||||
</div>
|
|
||||||
<div class="dashboard-response-actions" :class="{ conversational: isConversational }">
|
|
||||||
<router-link
|
|
||||||
:to="`/chat/${dashboardConvId}`"
|
|
||||||
class="btn-open-chat"
|
|
||||||
:class="{ prominent: isConversational }"
|
|
||||||
>{{ isConversational ? 'Continue the conversation →' : 'Think it through in Chat →' }}</router-link>
|
|
||||||
<button class="btn-clear-response" @click="clearDashboardResponse">Clear</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Upcoming events ────────────────────────────────────── -->
|
<!-- ── Upcoming events ────────────────────────────────────── -->
|
||||||
<div v-if="!loading && upcomingEvents.length" class="upcoming-events-section">
|
<div v-if="!loading && upcomingEvents.length" class="upcoming-events-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
@@ -608,92 +537,6 @@ function formatUpcomingTime(event: EventEntry): string {
|
|||||||
}
|
}
|
||||||
.quick-action-chip:disabled { opacity: 0.4; cursor: default; }
|
.quick-action-chip:disabled { opacity: 0.4; cursor: default; }
|
||||||
|
|
||||||
/* ─── Inline response ────────────────────────────────────────── */
|
|
||||||
.dashboard-response {
|
|
||||||
max-width: 720px;
|
|
||||||
margin: 0 auto 1.5rem;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
background: var(--color-bg-card);
|
|
||||||
border-left: 2px solid var(--color-primary);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
box-shadow: var(--color-bubble-asst-shadow);
|
|
||||||
}
|
|
||||||
.dashboard-response-query {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
.dashboard-tool-calls {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.35rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
.dashboard-response-text {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.55;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
.dashboard-response-text.streaming { color: var(--color-text-muted); }
|
|
||||||
.thinking-dots { display: inline-block; animation: blink 1.2s infinite; }
|
|
||||||
.dashboard-status-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
.dashboard-status-dot {
|
|
||||||
display: inline-block;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--color-primary);
|
|
||||||
animation: blink 1.2s infinite;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
@keyframes blink {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.3; }
|
|
||||||
}
|
|
||||||
.dashboard-response-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
}
|
|
||||||
.btn-open-chat {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--color-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.btn-open-chat:hover { text-decoration: underline; }
|
|
||||||
.btn-open-chat.prominent {
|
|
||||||
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
|
||||||
color: #fff;
|
|
||||||
padding: 0.35rem 0.85rem;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
box-shadow: 0 1px 6px rgba(99, 102, 241, 0.25);
|
|
||||||
}
|
|
||||||
.btn-open-chat.prominent:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
box-shadow: 0 3px 12px rgba(99, 102, 241, 0.45);
|
|
||||||
filter: brightness(1.08);
|
|
||||||
}
|
|
||||||
.btn-clear-response {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.btn-clear-response:hover { color: var(--color-text); }
|
|
||||||
|
|
||||||
/* ─── Skeleton loading ───────────────────────────────────────── */
|
/* ─── Skeleton loading ───────────────────────────────────────── */
|
||||||
.skeleton-hero {
|
.skeleton-hero {
|
||||||
height: 180px;
|
height: 180px;
|
||||||
|
|||||||
Reference in New Issue
Block a user