cognova 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +58 -0
- package/Claude/CLAUDE.md +92 -0
- package/Claude/hooks/lib/__init__.py +1 -0
- package/Claude/hooks/lib/hook_client.py +207 -0
- package/Claude/hooks/log-event.py +97 -0
- package/Claude/hooks/pre-compact.py +46 -0
- package/Claude/hooks/session-end.py +26 -0
- package/Claude/hooks/session-start.py +35 -0
- package/Claude/hooks/stop-extract.py +40 -0
- package/Claude/rules/frontmatter.md +54 -0
- package/Claude/rules/markdown.md +43 -0
- package/Claude/rules/note-organization.md +33 -0
- package/Claude/settings.json +54 -0
- package/Claude/skills/README.md +136 -0
- package/Claude/skills/_lib/__init__.py +1 -0
- package/Claude/skills/_lib/api.py +164 -0
- package/Claude/skills/_lib/output.py +95 -0
- package/Claude/skills/environment/SKILL.md +73 -0
- package/Claude/skills/environment/environment.py +239 -0
- package/Claude/skills/memory/SKILL.md +153 -0
- package/Claude/skills/memory/memory.py +270 -0
- package/Claude/skills/project/SKILL.md +105 -0
- package/Claude/skills/project/project.py +203 -0
- package/Claude/skills/skill-creator/SKILL.md +261 -0
- package/Claude/skills/task/SKILL.md +135 -0
- package/Claude/skills/task/task.py +310 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +39 -0
- package/app/assets/css/main.css +10 -0
- package/app/components/AppLogo.vue +40 -0
- package/app/components/AssistantPanel.client.vue +518 -0
- package/app/components/ConfirmModal.vue +84 -0
- package/app/components/TemplateMenu.vue +49 -0
- package/app/components/agents/AgentActivityChart.client.vue +105 -0
- package/app/components/agents/AgentActivityChart.server.vue +25 -0
- package/app/components/agents/AgentForm.vue +304 -0
- package/app/components/agents/AgentRunModal.vue +154 -0
- package/app/components/agents/AgentStatsCards.vue +98 -0
- package/app/components/chat/ChatInput.vue +85 -0
- package/app/components/chat/ConversationList.vue +78 -0
- package/app/components/chat/MessageBubble.vue +81 -0
- package/app/components/chat/StreamingMessage.vue +36 -0
- package/app/components/chat/ToolCallBlock.vue +77 -0
- package/app/components/editor/CodeEditor.client.vue +212 -0
- package/app/components/editor/CodeEditorFallback.vue +12 -0
- package/app/components/editor/DocumentEditor.vue +326 -0
- package/app/components/editor/DocumentMetadata.vue +140 -0
- package/app/components/editor/MarkdownEditor.vue +146 -0
- package/app/components/files/FileTree.vue +436 -0
- package/app/components/hooks/HookActivityChart.client.vue +117 -0
- package/app/components/hooks/HookActivityChart.server.vue +25 -0
- package/app/components/hooks/HookStatsCards.vue +63 -0
- package/app/components/hooks/RecentEventsTable.vue +123 -0
- package/app/components/hooks/ToolBreakdownTable.vue +72 -0
- package/app/components/search/DashboardSearch.vue +122 -0
- package/app/components/tasks/ProjectSelect.vue +35 -0
- package/app/components/tasks/TaskCard.vue +182 -0
- package/app/components/tasks/TaskDetail.vue +160 -0
- package/app/components/tasks/TaskForm.vue +280 -0
- package/app/components/tasks/TaskList.vue +69 -0
- package/app/components/view/ViewToc.vue +85 -0
- package/app/composables/useAgents.ts +153 -0
- package/app/composables/useAuth.ts +73 -0
- package/app/composables/useChat.ts +298 -0
- package/app/composables/useDocument.ts +141 -0
- package/app/composables/useEditor.ts +100 -0
- package/app/composables/useFileTree.ts +220 -0
- package/app/composables/useHookEvents.ts +68 -0
- package/app/composables/useMemories.ts +83 -0
- package/app/composables/useNotificationBus.ts +154 -0
- package/app/composables/usePreferences.ts +131 -0
- package/app/composables/useProjects.ts +97 -0
- package/app/composables/useSearch.ts +52 -0
- package/app/composables/useTasks.ts +201 -0
- package/app/composables/useTerminal.ts +135 -0
- package/app/layouts/auth.vue +20 -0
- package/app/layouts/dashboard.vue +186 -0
- package/app/layouts/view.vue +60 -0
- package/app/middleware/auth.ts +9 -0
- package/app/pages/agents/[id].vue +602 -0
- package/app/pages/agents/index.vue +412 -0
- package/app/pages/chat.vue +146 -0
- package/app/pages/dashboard.vue +80 -0
- package/app/pages/docs.vue +131 -0
- package/app/pages/hooks.vue +163 -0
- package/app/pages/index.vue +249 -0
- package/app/pages/login.vue +60 -0
- package/app/pages/memories.vue +282 -0
- package/app/pages/settings.vue +625 -0
- package/app/pages/tasks.vue +312 -0
- package/app/pages/view/[uuid].vue +376 -0
- package/dist/cli/index.js +2711 -0
- package/drizzle.config.ts +10 -0
- package/nuxt.config.ts +98 -0
- package/package.json +107 -0
- package/server/api/agents/[id]/cancel.post.ts +27 -0
- package/server/api/agents/[id]/run.post.ts +34 -0
- package/server/api/agents/[id]/runs.get.ts +45 -0
- package/server/api/agents/[id]/stats.get.ts +94 -0
- package/server/api/agents/[id].delete.ts +29 -0
- package/server/api/agents/[id].get.ts +25 -0
- package/server/api/agents/[id].patch.ts +55 -0
- package/server/api/agents/index.get.ts +15 -0
- package/server/api/agents/index.post.ts +48 -0
- package/server/api/agents/stats.get.ts +86 -0
- package/server/api/auth/[...all].ts +5 -0
- package/server/api/conversations/[id].delete.ts +16 -0
- package/server/api/conversations/[id].get.ts +34 -0
- package/server/api/conversations/index.get.ts +17 -0
- package/server/api/documents/[id]/index.delete.ts +47 -0
- package/server/api/documents/[id]/index.put.ts +102 -0
- package/server/api/documents/[id]/public.get.ts +60 -0
- package/server/api/documents/[id]/restore.post.ts +65 -0
- package/server/api/documents/by-path.post.ts +168 -0
- package/server/api/documents/index.get.ts +48 -0
- package/server/api/fs/delete.post.ts +41 -0
- package/server/api/fs/list.get.ts +99 -0
- package/server/api/fs/mkdir.post.ts +44 -0
- package/server/api/fs/move.post.ts +68 -0
- package/server/api/fs/read.post.ts +48 -0
- package/server/api/fs/rename.post.ts +55 -0
- package/server/api/fs/write.post.ts +51 -0
- package/server/api/health.get.ts +40 -0
- package/server/api/home.get.ts +26 -0
- package/server/api/hooks/events/index.get.ts +56 -0
- package/server/api/hooks/events/index.post.ts +36 -0
- package/server/api/hooks/stats.get.ts +99 -0
- package/server/api/memory/[id].delete.ts +26 -0
- package/server/api/memory/context.get.ts +83 -0
- package/server/api/memory/extract.post.ts +42 -0
- package/server/api/memory/search.get.ts +70 -0
- package/server/api/memory/store.post.ts +31 -0
- package/server/api/projects/[id]/index.delete.ts +40 -0
- package/server/api/projects/[id]/index.get.ts +25 -0
- package/server/api/projects/[id]/index.put.ts +50 -0
- package/server/api/projects/index.get.ts +20 -0
- package/server/api/projects/index.post.ts +34 -0
- package/server/api/secrets/[key].delete.ts +31 -0
- package/server/api/secrets/[key].get.ts +30 -0
- package/server/api/secrets/[key].put.ts +52 -0
- package/server/api/secrets/index.get.ts +20 -0
- package/server/api/secrets/index.post.ts +58 -0
- package/server/api/tasks/[id]/index.delete.ts +46 -0
- package/server/api/tasks/[id]/index.get.ts +24 -0
- package/server/api/tasks/[id]/index.put.ts +70 -0
- package/server/api/tasks/[id]/restore.post.ts +49 -0
- package/server/api/tasks/index.get.ts +53 -0
- package/server/api/tasks/index.post.ts +47 -0
- package/server/api/tasks/tags.get.ts +21 -0
- package/server/api/user/email.patch.ts +56 -0
- package/server/db/index.ts +76 -0
- package/server/db/migrate.ts +41 -0
- package/server/db/schema.ts +345 -0
- package/server/db/seed.ts +46 -0
- package/server/db/types.ts +28 -0
- package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
- package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
- package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
- package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
- package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
- package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
- package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
- package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
- package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
- package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
- package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
- package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
- package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
- package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
- package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
- package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
- package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
- package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
- package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
- package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
- package/server/drizzle/migrations/meta/_journal.json +76 -0
- package/server/middleware/auth.ts +79 -0
- package/server/plugins/00.env-validate.ts +38 -0
- package/server/plugins/01.api-token.ts +31 -0
- package/server/plugins/02.database.ts +54 -0
- package/server/plugins/03.file-watcher.ts +65 -0
- package/server/plugins/04.cron-agents.ts +26 -0
- package/server/routes/_ws/chat.ts +252 -0
- package/server/routes/notifications.ts +47 -0
- package/server/routes/terminal.ts +98 -0
- package/server/services/agent-executor.ts +218 -0
- package/server/services/cron-scheduler.ts +78 -0
- package/server/services/memory-extractor.ts +120 -0
- package/server/utils/agent-cleanup.ts +91 -0
- package/server/utils/agent-registry.ts +95 -0
- package/server/utils/auth.ts +33 -0
- package/server/utils/chat-session-manager.ts +59 -0
- package/server/utils/crypto.ts +40 -0
- package/server/utils/db-guard.ts +12 -0
- package/server/utils/db-state.ts +63 -0
- package/server/utils/document-sync.ts +207 -0
- package/server/utils/frontmatter.ts +84 -0
- package/server/utils/notification-bus.ts +60 -0
- package/server/utils/path-validator.ts +55 -0
- package/server/utils/pty-manager.ts +130 -0
- package/shared/types/index.ts +604 -0
- package/shared/utils/language-detection.ts +87 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Task, CreateTaskInput, UpdateTaskInput, TaskStatus } from '~~/shared/types'
|
|
3
|
+
|
|
4
|
+
definePageMeta({
|
|
5
|
+
layout: 'dashboard',
|
|
6
|
+
middleware: 'auth'
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
const route = useRoute()
|
|
10
|
+
const toast = useToast()
|
|
11
|
+
const { filteredTasks, tasks, loading, filters, taskCounts, fetchTasks, createTask, updateTask, deleteTask, toggleComplete } = useTasks()
|
|
12
|
+
const { projects, fetchProjects } = useProjects()
|
|
13
|
+
|
|
14
|
+
// Slideover state
|
|
15
|
+
const showForm = ref(false)
|
|
16
|
+
const editingTask = ref<Task | null>(null)
|
|
17
|
+
|
|
18
|
+
// Task detail modal
|
|
19
|
+
const showDetailModal = ref(false)
|
|
20
|
+
const selectedTask = ref<Task | null>(null)
|
|
21
|
+
|
|
22
|
+
// Delete confirmation modal
|
|
23
|
+
const showDeleteModal = ref(false)
|
|
24
|
+
const taskToDelete = ref<string | null>(null)
|
|
25
|
+
const deleteLoading = ref(false)
|
|
26
|
+
|
|
27
|
+
const ALL_VALUE = '__all__'
|
|
28
|
+
|
|
29
|
+
// Status filter options
|
|
30
|
+
const statusOptions = [
|
|
31
|
+
{ value: ALL_VALUE, label: 'All Status' },
|
|
32
|
+
{ value: 'todo', label: 'Todo' },
|
|
33
|
+
{ value: 'in_progress', label: 'In Progress' },
|
|
34
|
+
{ value: 'done', label: 'Done' },
|
|
35
|
+
{ value: 'blocked', label: 'Blocked' }
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
// Project filter options
|
|
39
|
+
const projectOptions = computed(() => [
|
|
40
|
+
{ value: ALL_VALUE, label: 'All Projects' },
|
|
41
|
+
...projects.value
|
|
42
|
+
.filter(p => !p.deletedAt)
|
|
43
|
+
.map(p => ({ value: p.id, label: p.name }))
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
// Persist filter preferences
|
|
47
|
+
const { taskStatusFilter, taskProjectFilter } = usePreferences()
|
|
48
|
+
|
|
49
|
+
// Filter values for selects (initialized from preferences)
|
|
50
|
+
const statusFilter = ref(taskStatusFilter.value === 'all' ? ALL_VALUE : taskStatusFilter.value)
|
|
51
|
+
const projectFilter = ref(taskProjectFilter.value || ALL_VALUE)
|
|
52
|
+
const searchQuery = ref('')
|
|
53
|
+
|
|
54
|
+
// Sync filter changes
|
|
55
|
+
watch(statusFilter, (value) => {
|
|
56
|
+
filters.status = value === ALL_VALUE ? undefined : value as TaskStatus
|
|
57
|
+
taskStatusFilter.value = value === ALL_VALUE ? 'all' : value as TaskStatus
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
watch(projectFilter, (value) => {
|
|
61
|
+
filters.projectId = value === ALL_VALUE ? undefined : value
|
|
62
|
+
taskProjectFilter.value = value === ALL_VALUE ? null : value
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
watch(searchQuery, (value) => {
|
|
66
|
+
filters.search = value || undefined
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Apply initial filters from preferences
|
|
70
|
+
if (statusFilter.value !== ALL_VALUE)
|
|
71
|
+
filters.status = statusFilter.value as TaskStatus
|
|
72
|
+
if (projectFilter.value !== ALL_VALUE)
|
|
73
|
+
filters.projectId = projectFilter.value
|
|
74
|
+
|
|
75
|
+
// Open form for new task
|
|
76
|
+
function openNewTaskForm() {
|
|
77
|
+
editingTask.value = null
|
|
78
|
+
showForm.value = true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Open form for editing
|
|
82
|
+
function openEditForm(task: Task) {
|
|
83
|
+
editingTask.value = task
|
|
84
|
+
showForm.value = true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// View task details
|
|
88
|
+
function viewTaskDetails(task: Task) {
|
|
89
|
+
selectedTask.value = task
|
|
90
|
+
showDetailModal.value = true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handle form submission
|
|
94
|
+
async function handleSubmit(data: CreateTaskInput | UpdateTaskInput) {
|
|
95
|
+
try {
|
|
96
|
+
if (editingTask.value) {
|
|
97
|
+
await updateTask(editingTask.value.id, data)
|
|
98
|
+
toast.add({
|
|
99
|
+
title: 'Task updated',
|
|
100
|
+
color: 'success',
|
|
101
|
+
icon: 'i-lucide-check'
|
|
102
|
+
})
|
|
103
|
+
} else {
|
|
104
|
+
await createTask(data as CreateTaskInput)
|
|
105
|
+
toast.add({
|
|
106
|
+
title: 'Task created',
|
|
107
|
+
color: 'success',
|
|
108
|
+
icon: 'i-lucide-check'
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
editingTask.value = null
|
|
112
|
+
} catch (e) {
|
|
113
|
+
toast.add({
|
|
114
|
+
title: 'Failed to save task',
|
|
115
|
+
description: e instanceof Error ? e.message : 'An unexpected error occurred',
|
|
116
|
+
color: 'error',
|
|
117
|
+
icon: 'i-lucide-alert-circle'
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Open delete confirmation
|
|
123
|
+
function confirmDelete(id: string) {
|
|
124
|
+
taskToDelete.value = id
|
|
125
|
+
showDeleteModal.value = true
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Handle confirmed delete
|
|
129
|
+
async function handleDeleteConfirm() {
|
|
130
|
+
if (!taskToDelete.value) return
|
|
131
|
+
|
|
132
|
+
deleteLoading.value = true
|
|
133
|
+
try {
|
|
134
|
+
await deleteTask(taskToDelete.value)
|
|
135
|
+
toast.add({
|
|
136
|
+
title: 'Task deleted',
|
|
137
|
+
color: 'success',
|
|
138
|
+
icon: 'i-lucide-trash'
|
|
139
|
+
})
|
|
140
|
+
showDeleteModal.value = false
|
|
141
|
+
taskToDelete.value = null
|
|
142
|
+
} catch (e) {
|
|
143
|
+
toast.add({
|
|
144
|
+
title: 'Failed to delete task',
|
|
145
|
+
description: e instanceof Error ? e.message : 'An unexpected error occurred',
|
|
146
|
+
color: 'error',
|
|
147
|
+
icon: 'i-lucide-alert-circle'
|
|
148
|
+
})
|
|
149
|
+
} finally {
|
|
150
|
+
deleteLoading.value = false
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Handle toggle complete
|
|
155
|
+
async function handleToggle(id: string) {
|
|
156
|
+
try {
|
|
157
|
+
await toggleComplete(id)
|
|
158
|
+
} catch (e) {
|
|
159
|
+
toast.add({
|
|
160
|
+
title: 'Failed to update task',
|
|
161
|
+
description: e instanceof Error ? e.message : 'An unexpected error occurred',
|
|
162
|
+
color: 'error',
|
|
163
|
+
icon: 'i-lucide-alert-circle'
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Handle selected query param from search
|
|
169
|
+
watch([() => route.query.selected, tasks], ([selectedId, taskList]) => {
|
|
170
|
+
if (selectedId && taskList.length > 0) {
|
|
171
|
+
const task = taskList.find(t => t.id === selectedId)
|
|
172
|
+
if (task) {
|
|
173
|
+
selectedTask.value = task
|
|
174
|
+
showDetailModal.value = true
|
|
175
|
+
// Clear the query param to avoid reopening on navigation
|
|
176
|
+
navigateTo('/tasks', { replace: true })
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}, { immediate: true })
|
|
180
|
+
|
|
181
|
+
// Handle action query param (for creating new task)
|
|
182
|
+
watch(() => route.query.action, (action) => {
|
|
183
|
+
if (action === 'new') {
|
|
184
|
+
openNewTaskForm()
|
|
185
|
+
navigateTo('/tasks', { replace: true })
|
|
186
|
+
}
|
|
187
|
+
}, { immediate: true })
|
|
188
|
+
|
|
189
|
+
// Load data on mount
|
|
190
|
+
onMounted(async () => {
|
|
191
|
+
await Promise.all([
|
|
192
|
+
fetchTasks(),
|
|
193
|
+
fetchProjects()
|
|
194
|
+
])
|
|
195
|
+
})
|
|
196
|
+
</script>
|
|
197
|
+
|
|
198
|
+
<template>
|
|
199
|
+
<div class="contents">
|
|
200
|
+
<UDashboardPanel
|
|
201
|
+
id="tasks"
|
|
202
|
+
grow
|
|
203
|
+
>
|
|
204
|
+
<template #header>
|
|
205
|
+
<UDashboardNavbar title="Tasks">
|
|
206
|
+
<template #left>
|
|
207
|
+
<div class="flex items-center gap-2 text-sm">
|
|
208
|
+
<UBadge
|
|
209
|
+
color="neutral"
|
|
210
|
+
variant="subtle"
|
|
211
|
+
>
|
|
212
|
+
{{ taskCounts.total }} total
|
|
213
|
+
</UBadge>
|
|
214
|
+
<UBadge
|
|
215
|
+
v-if="taskCounts.done > 0"
|
|
216
|
+
color="success"
|
|
217
|
+
variant="subtle"
|
|
218
|
+
>
|
|
219
|
+
{{ taskCounts.done }} done
|
|
220
|
+
</UBadge>
|
|
221
|
+
</div>
|
|
222
|
+
</template>
|
|
223
|
+
|
|
224
|
+
<template #right>
|
|
225
|
+
<UButton
|
|
226
|
+
icon="i-lucide-plus"
|
|
227
|
+
label="Add Task"
|
|
228
|
+
@click="openNewTaskForm"
|
|
229
|
+
/>
|
|
230
|
+
<UColorModeButton />
|
|
231
|
+
</template>
|
|
232
|
+
</UDashboardNavbar>
|
|
233
|
+
|
|
234
|
+
<UDashboardToolbar>
|
|
235
|
+
<template #left>
|
|
236
|
+
<USelect
|
|
237
|
+
v-model="statusFilter"
|
|
238
|
+
:items="statusOptions"
|
|
239
|
+
value-key="value"
|
|
240
|
+
class="w-36"
|
|
241
|
+
/>
|
|
242
|
+
|
|
243
|
+
<USelect
|
|
244
|
+
v-model="projectFilter"
|
|
245
|
+
:items="projectOptions"
|
|
246
|
+
value-key="value"
|
|
247
|
+
class="w-40"
|
|
248
|
+
/>
|
|
249
|
+
</template>
|
|
250
|
+
|
|
251
|
+
<template #right>
|
|
252
|
+
<UInput
|
|
253
|
+
v-model="searchQuery"
|
|
254
|
+
placeholder="Search tasks..."
|
|
255
|
+
icon="i-lucide-search"
|
|
256
|
+
class="w-64"
|
|
257
|
+
/>
|
|
258
|
+
</template>
|
|
259
|
+
</UDashboardToolbar>
|
|
260
|
+
</template>
|
|
261
|
+
|
|
262
|
+
<template #body>
|
|
263
|
+
<TasksTaskList
|
|
264
|
+
:tasks="filteredTasks"
|
|
265
|
+
:loading="loading"
|
|
266
|
+
@toggle="handleToggle"
|
|
267
|
+
@edit="openEditForm"
|
|
268
|
+
@delete="confirmDelete"
|
|
269
|
+
@create="openNewTaskForm"
|
|
270
|
+
@view="viewTaskDetails"
|
|
271
|
+
/>
|
|
272
|
+
</template>
|
|
273
|
+
</UDashboardPanel>
|
|
274
|
+
|
|
275
|
+
<!-- Task Form Slideover -->
|
|
276
|
+
<TasksTaskForm
|
|
277
|
+
v-model:open="showForm"
|
|
278
|
+
:task="editingTask"
|
|
279
|
+
:projects="projects"
|
|
280
|
+
@submit="handleSubmit"
|
|
281
|
+
/>
|
|
282
|
+
|
|
283
|
+
<!-- Task Detail Modal -->
|
|
284
|
+
<UModal
|
|
285
|
+
v-model:open="showDetailModal"
|
|
286
|
+
:title="selectedTask?.title"
|
|
287
|
+
>
|
|
288
|
+
<template #content>
|
|
289
|
+
<TasksTaskDetail
|
|
290
|
+
v-if="selectedTask"
|
|
291
|
+
:task="selectedTask"
|
|
292
|
+
@edit="openEditForm(selectedTask); showDetailModal = false"
|
|
293
|
+
@close="showDetailModal = false"
|
|
294
|
+
@toggle="handleToggle(selectedTask.id); showDetailModal = false"
|
|
295
|
+
/>
|
|
296
|
+
</template>
|
|
297
|
+
</UModal>
|
|
298
|
+
|
|
299
|
+
<!-- Delete Confirmation Modal -->
|
|
300
|
+
<ConfirmModal
|
|
301
|
+
v-model:open="showDeleteModal"
|
|
302
|
+
title="Delete Task"
|
|
303
|
+
description="Are you sure you want to delete this task? This action cannot be undone."
|
|
304
|
+
confirm-label="Delete"
|
|
305
|
+
confirm-color="error"
|
|
306
|
+
icon="i-lucide-trash-2"
|
|
307
|
+
:loading="deleteLoading"
|
|
308
|
+
@confirm="handleDeleteConfirm"
|
|
309
|
+
@cancel="taskToDelete = null"
|
|
310
|
+
/>
|
|
311
|
+
</div>
|
|
312
|
+
</template>
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { parseMarkdown } from '@nuxtjs/mdc/runtime'
|
|
3
|
+
import type { TocLink, PublicDocumentResponse } from '~~/shared/types'
|
|
4
|
+
import { detectLanguage } from '~~/shared/utils/language-detection'
|
|
5
|
+
|
|
6
|
+
definePageMeta({
|
|
7
|
+
layout: 'view'
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const route = useRoute()
|
|
11
|
+
const uuid = computed(() => route.params.uuid as string)
|
|
12
|
+
|
|
13
|
+
// Fetch document from public API
|
|
14
|
+
const { data, error, status } = await useFetch<{ data: PublicDocumentResponse }>(
|
|
15
|
+
`/api/documents/${uuid.value}/public`
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// Parse markdown for TOC generation
|
|
19
|
+
const tocLinks = ref<TocLink[]>([])
|
|
20
|
+
|
|
21
|
+
watch(() => data.value?.data?.content, async (content) => {
|
|
22
|
+
if (content) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = await parseMarkdown(content)
|
|
25
|
+
tocLinks.value = (parsed.toc?.links as TocLink[]) || []
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error('Failed to parse markdown for TOC:', e)
|
|
28
|
+
tocLinks.value = []
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}, { immediate: true })
|
|
32
|
+
|
|
33
|
+
// Error state helpers
|
|
34
|
+
const notFound = computed(() => error.value?.statusCode === 404)
|
|
35
|
+
const forbidden = computed(() => error.value?.statusCode === 403)
|
|
36
|
+
|
|
37
|
+
// File type helpers
|
|
38
|
+
const isMarkdown = computed(() => data.value?.data?.document?.fileType === 'markdown')
|
|
39
|
+
const isTextFile = computed(() => data.value?.data?.document?.fileType === 'text')
|
|
40
|
+
const isBinaryFile = computed(() => data.value?.data?.document?.fileType === 'binary')
|
|
41
|
+
const codeLanguage = computed(() => detectLanguage(data.value?.data?.document?.path || ''))
|
|
42
|
+
|
|
43
|
+
// SEO with robots control based on shareType
|
|
44
|
+
useSeoMeta({
|
|
45
|
+
title: () => data.value?.data?.document?.title || 'Document',
|
|
46
|
+
description: 'Shared document from Cognova',
|
|
47
|
+
robots: () => {
|
|
48
|
+
const shareType = data.value?.data?.document?.shareType
|
|
49
|
+
return shareType === 'public' ? 'index, follow' : 'noindex, nofollow'
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Check if user is authenticated (show edit button for any authenticated user)
|
|
54
|
+
const { isAuthenticated } = useAuth()
|
|
55
|
+
|
|
56
|
+
// View source toggle for markdown files (persisted)
|
|
57
|
+
const { viewSourceMode } = usePreferences()
|
|
58
|
+
const viewSource = ref(viewSourceMode.value)
|
|
59
|
+
watch(viewSource, v => viewSourceMode.value = v)
|
|
60
|
+
|
|
61
|
+
// Copy content to clipboard
|
|
62
|
+
const toast = useToast()
|
|
63
|
+
async function copyContent() {
|
|
64
|
+
const content = data.value?.data?.content
|
|
65
|
+
if (!content) return
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await navigator.clipboard.writeText(content)
|
|
69
|
+
toast.add({
|
|
70
|
+
title: 'Copied to clipboard',
|
|
71
|
+
icon: 'i-lucide-check',
|
|
72
|
+
color: 'success'
|
|
73
|
+
})
|
|
74
|
+
} catch {
|
|
75
|
+
toast.add({
|
|
76
|
+
title: 'Failed to copy',
|
|
77
|
+
icon: 'i-lucide-x',
|
|
78
|
+
color: 'error'
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Download content as file
|
|
84
|
+
function downloadContent() {
|
|
85
|
+
const content = data.value?.data?.content
|
|
86
|
+
const path = data.value?.data?.document?.path
|
|
87
|
+
if (!content || !path) return
|
|
88
|
+
|
|
89
|
+
const filename = path.split('/').pop() || 'document.txt'
|
|
90
|
+
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' })
|
|
91
|
+
const url = URL.createObjectURL(blob)
|
|
92
|
+
const link = document.createElement('a')
|
|
93
|
+
link.href = url
|
|
94
|
+
link.download = filename
|
|
95
|
+
link.click()
|
|
96
|
+
URL.revokeObjectURL(url)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Format dates for display
|
|
100
|
+
function formatDate(date: string | Date | null | undefined): string {
|
|
101
|
+
if (!date) return ''
|
|
102
|
+
return new Date(date).toLocaleDateString('en-US', {
|
|
103
|
+
year: 'numeric',
|
|
104
|
+
month: 'short',
|
|
105
|
+
day: 'numeric'
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<template>
|
|
111
|
+
<UContainer class="py-8">
|
|
112
|
+
<!-- Error: Not Found -->
|
|
113
|
+
<div
|
|
114
|
+
v-if="notFound"
|
|
115
|
+
class="py-24 text-center"
|
|
116
|
+
>
|
|
117
|
+
<UIcon
|
|
118
|
+
name="i-lucide-file-x"
|
|
119
|
+
class="size-16 mx-auto mb-4 text-dimmed"
|
|
120
|
+
/>
|
|
121
|
+
<h1 class="text-2xl font-bold mb-2">
|
|
122
|
+
Document Not Found
|
|
123
|
+
</h1>
|
|
124
|
+
<p class="text-dimmed mb-6">
|
|
125
|
+
This document doesn't exist or has been deleted.
|
|
126
|
+
</p>
|
|
127
|
+
<UButton
|
|
128
|
+
to="/"
|
|
129
|
+
variant="soft"
|
|
130
|
+
>
|
|
131
|
+
Go Home
|
|
132
|
+
</UButton>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<!-- Error: Forbidden -->
|
|
136
|
+
<div
|
|
137
|
+
v-else-if="forbidden"
|
|
138
|
+
class="py-24 text-center"
|
|
139
|
+
>
|
|
140
|
+
<UIcon
|
|
141
|
+
name="i-lucide-lock"
|
|
142
|
+
class="size-16 mx-auto mb-4 text-dimmed"
|
|
143
|
+
/>
|
|
144
|
+
<h1 class="text-2xl font-bold mb-2">
|
|
145
|
+
Access Denied
|
|
146
|
+
</h1>
|
|
147
|
+
<p class="text-dimmed mb-6">
|
|
148
|
+
This document is not publicly shared.
|
|
149
|
+
</p>
|
|
150
|
+
<UButton
|
|
151
|
+
to="/login"
|
|
152
|
+
variant="soft"
|
|
153
|
+
>
|
|
154
|
+
Sign In
|
|
155
|
+
</UButton>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<!-- Error: Binary File -->
|
|
159
|
+
<div
|
|
160
|
+
v-else-if="isBinaryFile && data?.data"
|
|
161
|
+
class="py-24 text-center"
|
|
162
|
+
>
|
|
163
|
+
<UIcon
|
|
164
|
+
name="i-lucide-file-warning"
|
|
165
|
+
class="size-16 mx-auto mb-4 text-dimmed"
|
|
166
|
+
/>
|
|
167
|
+
<h1 class="text-2xl font-bold mb-2">
|
|
168
|
+
Cannot Preview This File
|
|
169
|
+
</h1>
|
|
170
|
+
<p class="text-dimmed mb-6">
|
|
171
|
+
Binary files cannot be previewed.
|
|
172
|
+
</p>
|
|
173
|
+
<UButton
|
|
174
|
+
to="/"
|
|
175
|
+
variant="soft"
|
|
176
|
+
>
|
|
177
|
+
Go Home
|
|
178
|
+
</UButton>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- Loading -->
|
|
182
|
+
<div
|
|
183
|
+
v-else-if="status === 'pending'"
|
|
184
|
+
class="py-24 text-center"
|
|
185
|
+
>
|
|
186
|
+
<UIcon
|
|
187
|
+
name="i-lucide-loader-2"
|
|
188
|
+
class="size-8 mx-auto animate-spin text-dimmed"
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- Success: Text File View (CodeMirror) -->
|
|
193
|
+
<template v-else-if="isTextFile && data?.data?.content">
|
|
194
|
+
<!-- Document header with title and action buttons -->
|
|
195
|
+
<div class="flex items-center justify-between mb-4">
|
|
196
|
+
<h1 class="text-2xl font-bold">
|
|
197
|
+
{{ data.data.document.title }}
|
|
198
|
+
</h1>
|
|
199
|
+
<div class="flex items-center gap-2">
|
|
200
|
+
<UButton
|
|
201
|
+
icon="i-lucide-copy"
|
|
202
|
+
variant="ghost"
|
|
203
|
+
size="sm"
|
|
204
|
+
@click="copyContent"
|
|
205
|
+
>
|
|
206
|
+
Copy
|
|
207
|
+
</UButton>
|
|
208
|
+
<UButton
|
|
209
|
+
icon="i-lucide-download"
|
|
210
|
+
variant="ghost"
|
|
211
|
+
size="sm"
|
|
212
|
+
@click="downloadContent"
|
|
213
|
+
>
|
|
214
|
+
Download
|
|
215
|
+
</UButton>
|
|
216
|
+
<UButton
|
|
217
|
+
v-if="isAuthenticated"
|
|
218
|
+
icon="i-lucide-pencil"
|
|
219
|
+
variant="soft"
|
|
220
|
+
size="sm"
|
|
221
|
+
:to="`/docs?path=${encodeURIComponent(data.data.document.path)}`"
|
|
222
|
+
>
|
|
223
|
+
Edit
|
|
224
|
+
</UButton>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<!-- Code viewer -->
|
|
229
|
+
<div class="border border-default rounded-lg overflow-hidden">
|
|
230
|
+
<ClientOnly>
|
|
231
|
+
<EditorCodeEditor
|
|
232
|
+
:model-value="data.data.content"
|
|
233
|
+
:language="codeLanguage"
|
|
234
|
+
:read-only="true"
|
|
235
|
+
class="max-h-[80vh]"
|
|
236
|
+
/>
|
|
237
|
+
<template #fallback>
|
|
238
|
+
<EditorCodeEditorFallback class="h-96" />
|
|
239
|
+
</template>
|
|
240
|
+
</ClientOnly>
|
|
241
|
+
</div>
|
|
242
|
+
</template>
|
|
243
|
+
|
|
244
|
+
<!-- Success: Markdown Document View -->
|
|
245
|
+
<template v-else-if="isMarkdown && data?.data?.content">
|
|
246
|
+
<!-- Document header with title and action buttons -->
|
|
247
|
+
<div class="flex items-center justify-between mb-4">
|
|
248
|
+
<h1 class="text-2xl font-bold">
|
|
249
|
+
{{ data.data.document.title }}
|
|
250
|
+
</h1>
|
|
251
|
+
<div class="flex items-center gap-2">
|
|
252
|
+
<UButton
|
|
253
|
+
:icon="viewSource ? 'i-lucide-eye' : 'i-lucide-code'"
|
|
254
|
+
variant="ghost"
|
|
255
|
+
size="sm"
|
|
256
|
+
@click="viewSource = !viewSource"
|
|
257
|
+
>
|
|
258
|
+
{{ viewSource ? 'Preview' : 'Source' }}
|
|
259
|
+
</UButton>
|
|
260
|
+
<UButton
|
|
261
|
+
icon="i-lucide-copy"
|
|
262
|
+
variant="ghost"
|
|
263
|
+
size="sm"
|
|
264
|
+
@click="copyContent"
|
|
265
|
+
>
|
|
266
|
+
Copy
|
|
267
|
+
</UButton>
|
|
268
|
+
<UButton
|
|
269
|
+
icon="i-lucide-download"
|
|
270
|
+
variant="ghost"
|
|
271
|
+
size="sm"
|
|
272
|
+
@click="downloadContent"
|
|
273
|
+
>
|
|
274
|
+
Download
|
|
275
|
+
</UButton>
|
|
276
|
+
<UButton
|
|
277
|
+
v-if="isAuthenticated"
|
|
278
|
+
icon="i-lucide-pencil"
|
|
279
|
+
variant="soft"
|
|
280
|
+
size="sm"
|
|
281
|
+
:to="`/docs?path=${encodeURIComponent(data.data.document.path)}`"
|
|
282
|
+
>
|
|
283
|
+
Edit
|
|
284
|
+
</UButton>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<!-- Document metadata -->
|
|
289
|
+
<div class="flex flex-wrap items-center gap-4 text-sm text-dimmed mb-6">
|
|
290
|
+
<div
|
|
291
|
+
v-if="data.data.document.creatorName"
|
|
292
|
+
class="flex items-center gap-1.5"
|
|
293
|
+
>
|
|
294
|
+
<UIcon
|
|
295
|
+
name="i-lucide-user"
|
|
296
|
+
class="size-4"
|
|
297
|
+
/>
|
|
298
|
+
<span>{{ data.data.document.creatorName }}</span>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div class="flex items-center gap-1.5">
|
|
302
|
+
<UIcon
|
|
303
|
+
name="i-lucide-calendar"
|
|
304
|
+
class="size-4"
|
|
305
|
+
/>
|
|
306
|
+
<span>{{ formatDate(data.data.document.createdAt) }}</span>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div
|
|
310
|
+
v-if="data.data.document.modifiedAt"
|
|
311
|
+
class="flex items-center gap-1.5"
|
|
312
|
+
>
|
|
313
|
+
<UIcon
|
|
314
|
+
name="i-lucide-pencil"
|
|
315
|
+
class="size-4"
|
|
316
|
+
/>
|
|
317
|
+
<span>Updated {{ formatDate(data.data.document.modifiedAt) }}</span>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<div
|
|
321
|
+
v-if="data.data.document.tags?.length"
|
|
322
|
+
class="flex items-center gap-1.5"
|
|
323
|
+
>
|
|
324
|
+
<UIcon
|
|
325
|
+
name="i-lucide-tags"
|
|
326
|
+
class="size-4"
|
|
327
|
+
/>
|
|
328
|
+
<div class="flex gap-1">
|
|
329
|
+
<UBadge
|
|
330
|
+
v-for="tag in data.data.document.tags"
|
|
331
|
+
:key="tag"
|
|
332
|
+
color="neutral"
|
|
333
|
+
variant="subtle"
|
|
334
|
+
size="sm"
|
|
335
|
+
>
|
|
336
|
+
{{ tag }}
|
|
337
|
+
</UBadge>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<!-- Source view: CodeMirror -->
|
|
343
|
+
<div
|
|
344
|
+
v-if="viewSource"
|
|
345
|
+
class="border border-default rounded-lg overflow-hidden"
|
|
346
|
+
>
|
|
347
|
+
<ClientOnly>
|
|
348
|
+
<EditorCodeEditor
|
|
349
|
+
:model-value="data.data.content"
|
|
350
|
+
language="markdown"
|
|
351
|
+
:read-only="true"
|
|
352
|
+
class="max-h-[80vh]"
|
|
353
|
+
/>
|
|
354
|
+
<template #fallback>
|
|
355
|
+
<EditorCodeEditorFallback class="h-96" />
|
|
356
|
+
</template>
|
|
357
|
+
</ClientOnly>
|
|
358
|
+
</div>
|
|
359
|
+
|
|
360
|
+
<!-- Preview: Rendered markdown -->
|
|
361
|
+
<UPage v-else>
|
|
362
|
+
<template #left>
|
|
363
|
+
<UPageAside>
|
|
364
|
+
<ViewToc :links="tocLinks" />
|
|
365
|
+
</UPageAside>
|
|
366
|
+
</template>
|
|
367
|
+
|
|
368
|
+
<UPageBody>
|
|
369
|
+
<div class="prose prose-primary dark:prose-invert max-w-none">
|
|
370
|
+
<MDC :value="data.data.content" />
|
|
371
|
+
</div>
|
|
372
|
+
</UPageBody>
|
|
373
|
+
</UPage>
|
|
374
|
+
</template>
|
|
375
|
+
</UContainer>
|
|
376
|
+
</template>
|