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.
Files changed (205) hide show
  1. package/.env.example +58 -0
  2. package/Claude/CLAUDE.md +92 -0
  3. package/Claude/hooks/lib/__init__.py +1 -0
  4. package/Claude/hooks/lib/hook_client.py +207 -0
  5. package/Claude/hooks/log-event.py +97 -0
  6. package/Claude/hooks/pre-compact.py +46 -0
  7. package/Claude/hooks/session-end.py +26 -0
  8. package/Claude/hooks/session-start.py +35 -0
  9. package/Claude/hooks/stop-extract.py +40 -0
  10. package/Claude/rules/frontmatter.md +54 -0
  11. package/Claude/rules/markdown.md +43 -0
  12. package/Claude/rules/note-organization.md +33 -0
  13. package/Claude/settings.json +54 -0
  14. package/Claude/skills/README.md +136 -0
  15. package/Claude/skills/_lib/__init__.py +1 -0
  16. package/Claude/skills/_lib/api.py +164 -0
  17. package/Claude/skills/_lib/output.py +95 -0
  18. package/Claude/skills/environment/SKILL.md +73 -0
  19. package/Claude/skills/environment/environment.py +239 -0
  20. package/Claude/skills/memory/SKILL.md +153 -0
  21. package/Claude/skills/memory/memory.py +270 -0
  22. package/Claude/skills/project/SKILL.md +105 -0
  23. package/Claude/skills/project/project.py +203 -0
  24. package/Claude/skills/skill-creator/SKILL.md +261 -0
  25. package/Claude/skills/task/SKILL.md +135 -0
  26. package/Claude/skills/task/task.py +310 -0
  27. package/LICENSE +21 -0
  28. package/README.md +176 -0
  29. package/app/app.config.ts +8 -0
  30. package/app/app.vue +39 -0
  31. package/app/assets/css/main.css +10 -0
  32. package/app/components/AppLogo.vue +40 -0
  33. package/app/components/AssistantPanel.client.vue +518 -0
  34. package/app/components/ConfirmModal.vue +84 -0
  35. package/app/components/TemplateMenu.vue +49 -0
  36. package/app/components/agents/AgentActivityChart.client.vue +105 -0
  37. package/app/components/agents/AgentActivityChart.server.vue +25 -0
  38. package/app/components/agents/AgentForm.vue +304 -0
  39. package/app/components/agents/AgentRunModal.vue +154 -0
  40. package/app/components/agents/AgentStatsCards.vue +98 -0
  41. package/app/components/chat/ChatInput.vue +85 -0
  42. package/app/components/chat/ConversationList.vue +78 -0
  43. package/app/components/chat/MessageBubble.vue +81 -0
  44. package/app/components/chat/StreamingMessage.vue +36 -0
  45. package/app/components/chat/ToolCallBlock.vue +77 -0
  46. package/app/components/editor/CodeEditor.client.vue +212 -0
  47. package/app/components/editor/CodeEditorFallback.vue +12 -0
  48. package/app/components/editor/DocumentEditor.vue +326 -0
  49. package/app/components/editor/DocumentMetadata.vue +140 -0
  50. package/app/components/editor/MarkdownEditor.vue +146 -0
  51. package/app/components/files/FileTree.vue +436 -0
  52. package/app/components/hooks/HookActivityChart.client.vue +117 -0
  53. package/app/components/hooks/HookActivityChart.server.vue +25 -0
  54. package/app/components/hooks/HookStatsCards.vue +63 -0
  55. package/app/components/hooks/RecentEventsTable.vue +123 -0
  56. package/app/components/hooks/ToolBreakdownTable.vue +72 -0
  57. package/app/components/search/DashboardSearch.vue +122 -0
  58. package/app/components/tasks/ProjectSelect.vue +35 -0
  59. package/app/components/tasks/TaskCard.vue +182 -0
  60. package/app/components/tasks/TaskDetail.vue +160 -0
  61. package/app/components/tasks/TaskForm.vue +280 -0
  62. package/app/components/tasks/TaskList.vue +69 -0
  63. package/app/components/view/ViewToc.vue +85 -0
  64. package/app/composables/useAgents.ts +153 -0
  65. package/app/composables/useAuth.ts +73 -0
  66. package/app/composables/useChat.ts +298 -0
  67. package/app/composables/useDocument.ts +141 -0
  68. package/app/composables/useEditor.ts +100 -0
  69. package/app/composables/useFileTree.ts +220 -0
  70. package/app/composables/useHookEvents.ts +68 -0
  71. package/app/composables/useMemories.ts +83 -0
  72. package/app/composables/useNotificationBus.ts +154 -0
  73. package/app/composables/usePreferences.ts +131 -0
  74. package/app/composables/useProjects.ts +97 -0
  75. package/app/composables/useSearch.ts +52 -0
  76. package/app/composables/useTasks.ts +201 -0
  77. package/app/composables/useTerminal.ts +135 -0
  78. package/app/layouts/auth.vue +20 -0
  79. package/app/layouts/dashboard.vue +186 -0
  80. package/app/layouts/view.vue +60 -0
  81. package/app/middleware/auth.ts +9 -0
  82. package/app/pages/agents/[id].vue +602 -0
  83. package/app/pages/agents/index.vue +412 -0
  84. package/app/pages/chat.vue +146 -0
  85. package/app/pages/dashboard.vue +80 -0
  86. package/app/pages/docs.vue +131 -0
  87. package/app/pages/hooks.vue +163 -0
  88. package/app/pages/index.vue +249 -0
  89. package/app/pages/login.vue +60 -0
  90. package/app/pages/memories.vue +282 -0
  91. package/app/pages/settings.vue +625 -0
  92. package/app/pages/tasks.vue +312 -0
  93. package/app/pages/view/[uuid].vue +376 -0
  94. package/dist/cli/index.js +2711 -0
  95. package/drizzle.config.ts +10 -0
  96. package/nuxt.config.ts +98 -0
  97. package/package.json +107 -0
  98. package/server/api/agents/[id]/cancel.post.ts +27 -0
  99. package/server/api/agents/[id]/run.post.ts +34 -0
  100. package/server/api/agents/[id]/runs.get.ts +45 -0
  101. package/server/api/agents/[id]/stats.get.ts +94 -0
  102. package/server/api/agents/[id].delete.ts +29 -0
  103. package/server/api/agents/[id].get.ts +25 -0
  104. package/server/api/agents/[id].patch.ts +55 -0
  105. package/server/api/agents/index.get.ts +15 -0
  106. package/server/api/agents/index.post.ts +48 -0
  107. package/server/api/agents/stats.get.ts +86 -0
  108. package/server/api/auth/[...all].ts +5 -0
  109. package/server/api/conversations/[id].delete.ts +16 -0
  110. package/server/api/conversations/[id].get.ts +34 -0
  111. package/server/api/conversations/index.get.ts +17 -0
  112. package/server/api/documents/[id]/index.delete.ts +47 -0
  113. package/server/api/documents/[id]/index.put.ts +102 -0
  114. package/server/api/documents/[id]/public.get.ts +60 -0
  115. package/server/api/documents/[id]/restore.post.ts +65 -0
  116. package/server/api/documents/by-path.post.ts +168 -0
  117. package/server/api/documents/index.get.ts +48 -0
  118. package/server/api/fs/delete.post.ts +41 -0
  119. package/server/api/fs/list.get.ts +99 -0
  120. package/server/api/fs/mkdir.post.ts +44 -0
  121. package/server/api/fs/move.post.ts +68 -0
  122. package/server/api/fs/read.post.ts +48 -0
  123. package/server/api/fs/rename.post.ts +55 -0
  124. package/server/api/fs/write.post.ts +51 -0
  125. package/server/api/health.get.ts +40 -0
  126. package/server/api/home.get.ts +26 -0
  127. package/server/api/hooks/events/index.get.ts +56 -0
  128. package/server/api/hooks/events/index.post.ts +36 -0
  129. package/server/api/hooks/stats.get.ts +99 -0
  130. package/server/api/memory/[id].delete.ts +26 -0
  131. package/server/api/memory/context.get.ts +83 -0
  132. package/server/api/memory/extract.post.ts +42 -0
  133. package/server/api/memory/search.get.ts +70 -0
  134. package/server/api/memory/store.post.ts +31 -0
  135. package/server/api/projects/[id]/index.delete.ts +40 -0
  136. package/server/api/projects/[id]/index.get.ts +25 -0
  137. package/server/api/projects/[id]/index.put.ts +50 -0
  138. package/server/api/projects/index.get.ts +20 -0
  139. package/server/api/projects/index.post.ts +34 -0
  140. package/server/api/secrets/[key].delete.ts +31 -0
  141. package/server/api/secrets/[key].get.ts +30 -0
  142. package/server/api/secrets/[key].put.ts +52 -0
  143. package/server/api/secrets/index.get.ts +20 -0
  144. package/server/api/secrets/index.post.ts +58 -0
  145. package/server/api/tasks/[id]/index.delete.ts +46 -0
  146. package/server/api/tasks/[id]/index.get.ts +24 -0
  147. package/server/api/tasks/[id]/index.put.ts +70 -0
  148. package/server/api/tasks/[id]/restore.post.ts +49 -0
  149. package/server/api/tasks/index.get.ts +53 -0
  150. package/server/api/tasks/index.post.ts +47 -0
  151. package/server/api/tasks/tags.get.ts +21 -0
  152. package/server/api/user/email.patch.ts +56 -0
  153. package/server/db/index.ts +76 -0
  154. package/server/db/migrate.ts +41 -0
  155. package/server/db/schema.ts +345 -0
  156. package/server/db/seed.ts +46 -0
  157. package/server/db/types.ts +28 -0
  158. package/server/drizzle/migrations/0000_brown_george_stacy.sql +34 -0
  159. package/server/drizzle/migrations/0001_stormy_pyro.sql +16 -0
  160. package/server/drizzle/migrations/0002_clean_colossus.sql +50 -0
  161. package/server/drizzle/migrations/0003_fine_joystick.sql +12 -0
  162. package/server/drizzle/migrations/0004_tan_groot.sql +26 -0
  163. package/server/drizzle/migrations/0005_cloudy_lilith.sql +33 -0
  164. package/server/drizzle/migrations/0006_ordinary_retro_girl.sql +13 -0
  165. package/server/drizzle/migrations/0007_flowery_venus.sql +15 -0
  166. package/server/drizzle/migrations/0008_talented_zombie.sql +13 -0
  167. package/server/drizzle/migrations/0009_gray_shen.sql +15 -0
  168. package/server/drizzle/migrations/meta/0000_snapshot.json +230 -0
  169. package/server/drizzle/migrations/meta/0001_snapshot.json +306 -0
  170. package/server/drizzle/migrations/meta/0002_snapshot.json +615 -0
  171. package/server/drizzle/migrations/meta/0003_snapshot.json +730 -0
  172. package/server/drizzle/migrations/meta/0004_snapshot.json +916 -0
  173. package/server/drizzle/migrations/meta/0005_snapshot.json +1127 -0
  174. package/server/drizzle/migrations/meta/0006_snapshot.json +1213 -0
  175. package/server/drizzle/migrations/meta/0007_snapshot.json +1307 -0
  176. package/server/drizzle/migrations/meta/0008_snapshot.json +1390 -0
  177. package/server/drizzle/migrations/meta/0009_snapshot.json +1487 -0
  178. package/server/drizzle/migrations/meta/_journal.json +76 -0
  179. package/server/middleware/auth.ts +79 -0
  180. package/server/plugins/00.env-validate.ts +38 -0
  181. package/server/plugins/01.api-token.ts +31 -0
  182. package/server/plugins/02.database.ts +54 -0
  183. package/server/plugins/03.file-watcher.ts +65 -0
  184. package/server/plugins/04.cron-agents.ts +26 -0
  185. package/server/routes/_ws/chat.ts +252 -0
  186. package/server/routes/notifications.ts +47 -0
  187. package/server/routes/terminal.ts +98 -0
  188. package/server/services/agent-executor.ts +218 -0
  189. package/server/services/cron-scheduler.ts +78 -0
  190. package/server/services/memory-extractor.ts +120 -0
  191. package/server/utils/agent-cleanup.ts +91 -0
  192. package/server/utils/agent-registry.ts +95 -0
  193. package/server/utils/auth.ts +33 -0
  194. package/server/utils/chat-session-manager.ts +59 -0
  195. package/server/utils/crypto.ts +40 -0
  196. package/server/utils/db-guard.ts +12 -0
  197. package/server/utils/db-state.ts +63 -0
  198. package/server/utils/document-sync.ts +207 -0
  199. package/server/utils/frontmatter.ts +84 -0
  200. package/server/utils/notification-bus.ts +60 -0
  201. package/server/utils/path-validator.ts +55 -0
  202. package/server/utils/pty-manager.ts +130 -0
  203. package/shared/types/index.ts +604 -0
  204. package/shared/utils/language-detection.ts +87 -0
  205. 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>