popilot 0.6.0 → 0.8.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 (165) hide show
  1. package/bin/cli.mjs +204 -2
  2. package/lib/doctor.mjs +38 -1
  3. package/lib/hydrate.mjs +15 -0
  4. package/lib/scaffold.mjs +5 -0
  5. package/lib/setup-wizard.mjs +35 -2
  6. package/package.json +1 -1
  7. package/scaffold/.context/project.yaml.example +19 -0
  8. package/scaffold/mcp-notification-server/package.json +18 -0
  9. package/scaffold/mcp-notification-server/src/index.ts +275 -0
  10. package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
  11. package/scaffold/mcp-notification-server/tsconfig.json +14 -0
  12. package/scaffold/mcp-pm/package.json +19 -0
  13. package/scaffold/mcp-pm/src/api-client.ts +69 -0
  14. package/scaffold/mcp-pm/src/index.ts +660 -0
  15. package/scaffold/mcp-pm/tsconfig.json +14 -0
  16. package/scaffold/pm-api/package.json +21 -0
  17. package/scaffold/pm-api/sql/001-memo-v2.sql +49 -0
  18. package/scaffold/pm-api/sql/002-notifications.sql +18 -0
  19. package/scaffold/pm-api/sql/003-content.sql +66 -0
  20. package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
  21. package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -0
  22. package/scaffold/pm-api/sql/schema-core.sql +331 -0
  23. package/scaffold/pm-api/sql/schema-docs.sql +25 -0
  24. package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
  25. package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
  26. package/scaffold/pm-api/src/auth.ts +28 -0
  27. package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
  28. package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
  29. package/scaffold/pm-api/src/db/adapter.ts +36 -0
  30. package/scaffold/pm-api/src/db/turso.ts +147 -0
  31. package/scaffold/pm-api/src/index.ts +114 -0
  32. package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
  33. package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
  34. package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
  35. package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
  36. package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
  37. package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
  38. package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
  39. package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
  40. package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
  41. package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
  42. package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
  43. package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
  44. package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
  45. package/scaffold/pm-api/src/mcp.ts +871 -0
  46. package/scaffold/pm-api/src/nudge.ts +283 -0
  47. package/scaffold/pm-api/src/routes/auth.ts +32 -0
  48. package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
  49. package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
  50. package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
  51. package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
  52. package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
  53. package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
  54. package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
  55. package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
  56. package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
  57. package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
  58. package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
  59. package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
  60. package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
  61. package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
  62. package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
  63. package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
  64. package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
  65. package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
  66. package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
  67. package/scaffold/pm-api/src/types.ts +11 -0
  68. package/scaffold/pm-api/src/utils/activity.ts +22 -0
  69. package/scaffold/pm-api/src/utils/admin.ts +9 -0
  70. package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
  71. package/scaffold/pm-api/src/utils/assignee.ts +69 -0
  72. package/scaffold/pm-api/src/utils/db.ts +45 -0
  73. package/scaffold/pm-api/src/utils/initiative.ts +23 -0
  74. package/scaffold/pm-api/src/utils/retro-link.ts +32 -0
  75. package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
  76. package/scaffold/pm-api/tsconfig.json +15 -0
  77. package/scaffold/pm-api/wrangler.toml.hbs +11 -0
  78. package/scaffold/spec-site/package-lock.json +892 -0
  79. package/scaffold/spec-site/package.json +15 -1
  80. package/scaffold/spec-site/src/api/types.ts +6 -0
  81. package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
  82. package/scaffold/spec-site/src/components/AuthGate.vue +117 -0
  83. package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
  84. package/scaffold/spec-site/src/components/DocComments.vue +137 -0
  85. package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
  86. package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
  87. package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
  88. package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
  89. package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
  90. package/scaffold/spec-site/src/components/Icon.vue +58 -0
  91. package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
  92. package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
  93. package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
  94. package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
  95. package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
  96. package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
  97. package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
  98. package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
  99. package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
  100. package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
  101. package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
  102. package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
  103. package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
  104. package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
  105. package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
  106. package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
  107. package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
  108. package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
  109. package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
  110. package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
  111. package/scaffold/spec-site/src/composables/useMemo.ts +39 -0
  112. package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
  113. package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
  114. package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
  115. package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
  116. package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
  117. package/scaffold/spec-site/src/composables/useTurso.ts +17 -0
  118. package/scaffold/spec-site/src/composables/useUser.ts +19 -1
  119. package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
  120. package/scaffold/spec-site/src/features.ts +108 -0
  121. package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
  122. package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
  123. package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
  124. package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
  125. package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -0
  126. package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
  127. package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
  128. package/scaffold/spec-site/src/pages/DocsEditor.vue +119 -0
  129. package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
  130. package/scaffold/spec-site/src/pages/DocsPage.vue +444 -0
  131. package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
  132. package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
  133. package/scaffold/spec-site/src/pages/MemosPage.vue +857 -0
  134. package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
  135. package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
  136. package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
  137. package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
  138. package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
  139. package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
  140. package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
  141. package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -0
  142. package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
  143. package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
  144. package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
  145. package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
  146. package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
  147. package/scaffold/spec-site/src/pages/board/KanbanBoard.vue +93 -0
  148. package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
  149. package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
  150. package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
  151. package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
  152. package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
  153. package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
  154. package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
  155. package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
  156. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
  157. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
  158. package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
  159. package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
  160. package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
  161. package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
  162. package/scaffold/spec-site/src/router.ts +141 -0
  163. package/scaffold/spec-site/src/styles/buttons.css +124 -0
  164. package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
  165. package/scaffold/spec-site/src/utils/timezone.ts +18 -0
@@ -0,0 +1,294 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue'
3
+ import { apiGet, apiPost, apiPatch, isStaticMode } from '@/api/client'
4
+ import MemberSelect from '@/components/MemberSelect.vue'
5
+
6
+ interface Meeting { id: number; title: string; date: string; participants: string | null; created_by: string }
7
+
8
+ const meetings = ref<Meeting[]>([])
9
+ const meetingsLoading = ref(true)
10
+ const showCreate = ref(false)
11
+ const form = ref({ title: '', date: new Date().toISOString().split('T')[0], rawTranscript: '' })
12
+ const selectedParticipants = ref<string[]>([])
13
+ const selectedMeeting = ref<Record<string, unknown> | null>(null)
14
+
15
+ const structurizing = ref(false)
16
+ const uploading = ref(false)
17
+
18
+ const editSummary = ref('')
19
+ const editAgenda = ref('')
20
+ const editDecisions = ref('')
21
+ const editActionItems = ref('')
22
+
23
+ async function loadMeetings() {
24
+ if (isStaticMode()) { meetingsLoading.value = false; return }
25
+ meetingsLoading.value = true
26
+ const { data } = await apiGet<{ meetings: Meeting[] }>('/api/v2/meetings')
27
+ if (data?.meetings) meetings.value = data.meetings
28
+ meetingsLoading.value = false
29
+ }
30
+
31
+ async function createMeeting() {
32
+ await apiPost('/api/v2/meetings', {
33
+ ...form.value,
34
+ participants: selectedParticipants.value.join(', ') || null,
35
+ })
36
+ form.value = { title: '', date: new Date().toISOString().split('T')[0], rawTranscript: '' }
37
+ selectedParticipants.value = []
38
+ showCreate.value = false
39
+ await loadMeetings()
40
+ }
41
+
42
+ async function viewMeeting(id: number) {
43
+ const { data } = await apiGet<{ meeting: Record<string, unknown> }>(`/api/v2/meetings/${id}`)
44
+ if (data?.meeting) {
45
+ selectedMeeting.value = data.meeting
46
+ editSummary.value = (data.meeting.summary as string) ?? ''
47
+ editAgenda.value = (data.meeting.agenda as string) ?? ''
48
+ editDecisions.value = (data.meeting.decisions as string) ?? ''
49
+ editActionItems.value = (data.meeting.action_items as string) ?? ''
50
+ }
51
+ }
52
+
53
+ async function saveMeetingEdits() {
54
+ if (!selectedMeeting.value) return
55
+ await apiPatch(`/api/v2/meetings/${selectedMeeting.value.id}`, {
56
+ summary: editSummary.value || null,
57
+ agenda: editAgenda.value || null,
58
+ decisions: editDecisions.value || null,
59
+ actionItems: editActionItems.value || null,
60
+ })
61
+ await viewMeeting(selectedMeeting.value.id as number)
62
+ }
63
+
64
+ async function uploadAudio(e: Event, meetingId: number) {
65
+ const input = e.target as HTMLInputElement
66
+ const file = input.files?.[0]
67
+ if (!file) return
68
+ if (file.size > 25 * 1024 * 1024) { alert('File size exceeds 25MB limit'); return }
69
+
70
+ uploading.value = true
71
+ const formData = new FormData()
72
+ formData.append('file', file)
73
+
74
+ const url = import.meta.env.VITE_API_URL as string
75
+ const token = localStorage.getItem('spec-auth-token') || ''
76
+ const res = await fetch(`${url}/api/v2/meetings/${meetingId}/transcribe`, {
77
+ method: 'POST',
78
+ headers: { 'Authorization': `Bearer ${token}` },
79
+ body: formData,
80
+ })
81
+ const data = await res.json()
82
+ uploading.value = false
83
+ input.value = ''
84
+
85
+ if (data.error) { alert(data.error); return }
86
+ alert('Transcription complete')
87
+ await viewMeeting(meetingId)
88
+ }
89
+
90
+ async function structurize(id: number) {
91
+ if (!selectedMeeting.value?.raw_transcript) { alert('No transcript available'); return }
92
+
93
+ const { data: settingsData } = await apiGet<{ settings: Record<string, string> }>('/api/v2/admin/settings')
94
+ const settings = settingsData?.settings ?? {}
95
+ const apiKey = settings.llm_api_key
96
+ if (!apiKey) { alert('Please set an API key in /admin settings'); return }
97
+
98
+ const provider = settings.llm_provider ?? (apiKey.startsWith('sk-ant') ? 'anthropic' : apiKey.startsWith('AI') ? 'gemini' : 'openai')
99
+ const model = settings.llm_model ?? (provider === 'openai' ? 'gpt-4o-mini' : provider === 'gemini' ? 'gemini-2.0-flash' : 'claude-sonnet-4-20250514')
100
+ const transcript = selectedMeeting.value.raw_transcript as string
101
+
102
+ const systemPrompt = `You are an expert at structuring meeting transcripts.
103
+ Analyze the transcript below and return a JSON object:
104
+ {
105
+ "summary": "One-line summary",
106
+ "agenda": "Agenda items (newline-separated)",
107
+ "decisions": "Decisions made (newline-separated)",
108
+ "action_items": "Action items (newline-separated, include assignee on each line)"
109
+ }
110
+ Return only JSON.`
111
+
112
+ structurizing.value = true
113
+ try {
114
+ let result: { summary?: string; agenda?: string; decisions?: string; action_items?: string }
115
+
116
+ if (provider === 'openai') {
117
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
120
+ body: JSON.stringify({
121
+ model,
122
+ messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: transcript }],
123
+ response_format: { type: 'json_object' },
124
+ }),
125
+ })
126
+ const data = await res.json() as { choices?: Array<{ message?: { content?: string } }> }
127
+ result = JSON.parse(data.choices?.[0]?.message?.content ?? '{}')
128
+ } else if (provider === 'gemini') {
129
+ const geminiModel = model || 'gemini-2.0-flash'
130
+ const res = await fetch(
131
+ `https://generativelanguage.googleapis.com/v1beta/models/${geminiModel}:generateContent?key=${apiKey}`,
132
+ {
133
+ method: 'POST',
134
+ headers: { 'Content-Type': 'application/json' },
135
+ body: JSON.stringify({
136
+ contents: [{ parts: [{ text: `${systemPrompt}\n\n${transcript}` }] }],
137
+ generationConfig: { responseMimeType: 'application/json' },
138
+ }),
139
+ },
140
+ )
141
+ const data = await res.json() as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }> }
142
+ const geminiText = data.candidates?.[0]?.content?.parts?.[0]?.text ?? '{}'
143
+ const gMatch = geminiText.match(/\{[\s\S]*\}/)
144
+ result = JSON.parse(gMatch?.[0] ?? '{}')
145
+ } else {
146
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
147
+ method: 'POST',
148
+ headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
149
+ body: JSON.stringify({
150
+ model, max_tokens: 4096, system: systemPrompt,
151
+ messages: [{ role: 'user', content: transcript }],
152
+ }),
153
+ })
154
+ const data = await res.json() as { content?: Array<{ text?: string }> }
155
+ const text = data.content?.[0]?.text ?? '{}'
156
+ const jsonMatch = text.match(/\{[\s\S]*\}/)
157
+ result = JSON.parse(jsonMatch?.[0] ?? '{}')
158
+ }
159
+
160
+ editSummary.value = result.summary ?? ''
161
+ editAgenda.value = result.agenda ?? ''
162
+ editDecisions.value = result.decisions ?? ''
163
+ editActionItems.value = result.action_items ?? ''
164
+ await saveMeetingEdits()
165
+ await viewMeeting(id)
166
+ } catch (e) {
167
+ alert(`AI structuring failed: ${String(e)}`)
168
+ } finally { structurizing.value = false }
169
+ }
170
+
171
+ async function createTasks(id: number) {
172
+ const { data } = await apiPost(`/api/v2/meetings/${id}/create-tasks`, {})
173
+ if (data) alert(`${(data as any).created} tasks created`)
174
+ }
175
+
176
+ onMounted(loadMeetings)
177
+ </script>
178
+
179
+ <template>
180
+ <div class="meetings-page">
181
+ <div class="meetings-header">
182
+ <h1>Meeting Notes</h1>
183
+ <button class="btn btn--primary" @click="showCreate = !showCreate">+ New Meeting</button>
184
+ </div>
185
+
186
+ <!-- Create form -->
187
+ <div v-if="showCreate" class="create-form glass-card">
188
+ <input v-model="form.title" class="form-input" placeholder="Meeting title" />
189
+ <input v-model="form.date" type="date" class="form-input" />
190
+ <div class="participants-select">
191
+ <span class="participants-label">Participants:</span>
192
+ <MemberSelect v-model="selectedParticipants" />
193
+ </div>
194
+ <textarea v-model="form.rawTranscript" class="form-textarea" placeholder="Paste transcript here..." rows="8"></textarea>
195
+ <button class="btn btn--primary" @click="createMeeting">Save</button>
196
+ </div>
197
+
198
+ <!-- List -->
199
+ <div class="meetings-list">
200
+ <div v-for="m in meetings" :key="m.id" class="meeting-card glass-card" @click="viewMeeting(m.id)">
201
+ <div class="meeting-title">{{ m.title }}</div>
202
+ <div class="meeting-meta">
203
+ <span>{{ m.date }}</span>
204
+ <span v-if="m.participants">{{ m.participants }}</span>
205
+ <span>{{ m.created_by }}</span>
206
+ </div>
207
+ </div>
208
+ <div v-if="meetingsLoading" class="empty">Loading...</div>
209
+ <div v-else-if="!meetings.length" class="empty">No meeting notes yet. Create one to get started.</div>
210
+ </div>
211
+
212
+ <!-- Detail -->
213
+ <div v-if="selectedMeeting" class="meeting-detail glass-card">
214
+ <div class="detail-header">
215
+ <h2>{{ selectedMeeting.title }}</h2>
216
+ <button class="btn btn--sm" @click="selectedMeeting = null">Close</button>
217
+ </div>
218
+ <div class="detail-meta">{{ selectedMeeting.date }} | {{ selectedMeeting.participants }}</div>
219
+
220
+ <!-- Structured results (editable) -->
221
+ <div class="detail-section">
222
+ <h3>Summary</h3>
223
+ <textarea v-model="editSummary" class="edit-textarea" rows="2" placeholder="Meeting summary"></textarea>
224
+ </div>
225
+ <div class="detail-section">
226
+ <h3>Agenda</h3>
227
+ <textarea v-model="editAgenda" class="edit-textarea" rows="3" placeholder="Agenda items (one per line)"></textarea>
228
+ </div>
229
+ <div class="detail-section">
230
+ <h3>Decisions</h3>
231
+ <textarea v-model="editDecisions" class="edit-textarea" rows="3" placeholder="Decisions (one per line)"></textarea>
232
+ </div>
233
+ <div class="detail-section">
234
+ <h3>Action Items</h3>
235
+ <textarea v-model="editActionItems" class="edit-textarea" rows="3" placeholder="Action items (one per line, include assignee)"></textarea>
236
+ <button v-if="editActionItems" class="btn btn--sm btn--primary" @click="createTasks(selectedMeeting.id as number)">Auto-create Tasks</button>
237
+ </div>
238
+
239
+ <div class="detail-actions">
240
+ <button class="btn btn--primary" @click="saveMeetingEdits">Save</button>
241
+ <label class="btn btn--sm upload-btn">
242
+ Upload Audio
243
+ <input type="file" accept=".mp3,.wav,.m4a,.webm,.ogg" hidden @change="uploadAudio($event, selectedMeeting.id as number)" />
244
+ </label>
245
+ <span v-if="uploading" class="upload-status">Transcribing...</span>
246
+ <button v-if="selectedMeeting.raw_transcript" class="btn btn--sm" :disabled="structurizing" @click="structurize(selectedMeeting.id as number)">
247
+ {{ structurizing ? 'AI Structuring...' : 'AI Structure' }}
248
+ </button>
249
+ </div>
250
+
251
+ <div v-if="selectedMeeting.raw_transcript" class="detail-section">
252
+ <h3>Transcript</h3>
253
+ <pre class="transcript">{{ selectedMeeting.raw_transcript }}</pre>
254
+ </div>
255
+ </div>
256
+ </div>
257
+ </template>
258
+
259
+ <style scoped>
260
+ .meetings-page { max-width: 800px; margin: 0 auto; padding: 24px; min-height: 100vh; }
261
+ .meetings-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
262
+ .meetings-header h1 { font-size: 22px; font-weight: 700; }
263
+ .create-form { padding: 20px; display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
264
+ .form-input { padding: 8px 12px; border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; font-size: 14px; }
265
+ .form-textarea { padding: 8px 12px; border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; font-size: 13px; font-family: monospace; resize: vertical; }
266
+ .meetings-list { display: flex; flex-direction: column; gap: 8px; }
267
+ .meeting-card { padding: 16px; cursor: pointer; }
268
+ .meeting-card:hover { transform: translateY(-1px); }
269
+ .meeting-title { font-size: 15px; font-weight: 600; }
270
+ .meeting-meta { font-size: 12px; color: var(--text-secondary); display: flex; gap: 12px; margin-top: 4px; }
271
+ .meeting-detail { padding: 24px; margin-top: 20px; }
272
+ .detail-header { display: flex; justify-content: space-between; align-items: center; }
273
+ .detail-header h2 { font-size: 18px; }
274
+ .detail-meta { font-size: 13px; color: var(--text-secondary); margin: 8px 0 16px; }
275
+ .detail-section { margin-bottom: 16px; }
276
+ .detail-section h3 { font-size: 14px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
277
+ .detail-section pre { font-size: 13px; white-space: pre-wrap; line-height: 1.6; }
278
+ .transcript { max-height: 300px; overflow-y: auto; background: rgba(0,0,0,0.03); padding: 12px; border-radius: 8px; }
279
+ .empty { color: var(--text-muted); padding: 20px; text-align: center; }
280
+ .edit-textarea { width: 100%; padding: 8px 12px; border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; font-size: 13px; resize: vertical; font-family: inherit; }
281
+ .detail-actions { display: flex; gap: 8px; margin: 16px 0; flex-wrap: wrap; }
282
+ .participants-select { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
283
+ .participants-label { font-size: 13px; color: var(--text-secondary); font-weight: 500; flex-shrink: 0; }
284
+ .btn { padding: 8px 16px; border-radius: 8px; border: none; font-size: 14px; font-weight: 500; cursor: pointer; }
285
+ .btn--primary { background: var(--primary); color: #fff; }
286
+ .btn--sm { padding: 4px 10px; font-size: 12px; }
287
+ .glass-card {
288
+ background: rgba(255,255,255,0.25); backdrop-filter: blur(40px) saturate(1.8);
289
+ border: 1px solid rgba(255,255,255,0.45); border-radius: 16px;
290
+ box-shadow: 0 2px 12px rgba(0,0,0,0.03), inset 0 1px 0 rgba(255,255,255,0.5);
291
+ }
292
+ .upload-btn { cursor: pointer; background: #eff6ff; color: #2563eb; border: 1px solid #bfdbfe; }
293
+ .upload-status { font-size: 12px; color: #f59e0b; }
294
+ </style>