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.
- package/bin/cli.mjs +204 -2
- package/lib/doctor.mjs +38 -1
- package/lib/hydrate.mjs +15 -0
- package/lib/scaffold.mjs +5 -0
- package/lib/setup-wizard.mjs +35 -2
- package/package.json +1 -1
- package/scaffold/.context/project.yaml.example +19 -0
- package/scaffold/mcp-notification-server/package.json +18 -0
- package/scaffold/mcp-notification-server/src/index.ts +275 -0
- package/scaffold/mcp-notification-server/src/turso-client.ts +142 -0
- package/scaffold/mcp-notification-server/tsconfig.json +14 -0
- package/scaffold/mcp-pm/package.json +19 -0
- package/scaffold/mcp-pm/src/api-client.ts +69 -0
- package/scaffold/mcp-pm/src/index.ts +660 -0
- package/scaffold/mcp-pm/tsconfig.json +14 -0
- package/scaffold/pm-api/package.json +21 -0
- package/scaffold/pm-api/sql/001-memo-v2.sql +49 -0
- package/scaffold/pm-api/sql/002-notifications.sql +18 -0
- package/scaffold/pm-api/sql/003-content.sql +66 -0
- package/scaffold/pm-api/sql/004-agent-events.sql +21 -0
- package/scaffold/pm-api/sql/005-epic-sprint-decoupling.sql +6 -0
- package/scaffold/pm-api/sql/schema-core.sql +331 -0
- package/scaffold/pm-api/sql/schema-docs.sql +25 -0
- package/scaffold/pm-api/sql/schema-meetings.sql +17 -0
- package/scaffold/pm-api/sql/schema-rewards.sql +16 -0
- package/scaffold/pm-api/src/auth.ts +28 -0
- package/scaffold/pm-api/src/blockchain/adapter.ts +20 -0
- package/scaffold/pm-api/src/blockchain/tron.ts +62 -0
- package/scaffold/pm-api/src/db/adapter.ts +36 -0
- package/scaffold/pm-api/src/db/turso.ts +147 -0
- package/scaffold/pm-api/src/index.ts +114 -0
- package/scaffold/pm-api/src/mcp-tools/dashboard.ts +40 -0
- package/scaffold/pm-api/src/mcp-tools/epic.ts +67 -0
- package/scaffold/pm-api/src/mcp-tools/event.ts +89 -0
- package/scaffold/pm-api/src/mcp-tools/index.ts +11 -0
- package/scaffold/pm-api/src/mcp-tools/initiative.ts +51 -0
- package/scaffold/pm-api/src/mcp-tools/memo.ts +164 -0
- package/scaffold/pm-api/src/mcp-tools/notification.ts +37 -0
- package/scaffold/pm-api/src/mcp-tools/retro.ts +183 -0
- package/scaffold/pm-api/src/mcp-tools/sprint.ts +204 -0
- package/scaffold/pm-api/src/mcp-tools/standup.ts +136 -0
- package/scaffold/pm-api/src/mcp-tools/story.ts +230 -0
- package/scaffold/pm-api/src/mcp-tools/task.ts +187 -0
- package/scaffold/pm-api/src/mcp-tools/utils.ts +83 -0
- package/scaffold/pm-api/src/mcp.ts +871 -0
- package/scaffold/pm-api/src/nudge.ts +283 -0
- package/scaffold/pm-api/src/routes/auth.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-activity.ts +27 -0
- package/scaffold/pm-api/src/routes/v2-admin.ts +165 -0
- package/scaffold/pm-api/src/routes/v2-dashboard.ts +189 -0
- package/scaffold/pm-api/src/routes/v2-docs.ts +34 -0
- package/scaffold/pm-api/src/routes/v2-initiatives.ts +118 -0
- package/scaffold/pm-api/src/routes/v2-kickoff.ts +265 -0
- package/scaffold/pm-api/src/routes/v2-meetings.ts +324 -0
- package/scaffold/pm-api/src/routes/v2-memos.ts +257 -0
- package/scaffold/pm-api/src/routes/v2-nav.ts +260 -0
- package/scaffold/pm-api/src/routes/v2-notifications.ts +79 -0
- package/scaffold/pm-api/src/routes/v2-page-content.ts +35 -0
- package/scaffold/pm-api/src/routes/v2-pm.ts +380 -0
- package/scaffold/pm-api/src/routes/v2-policy.ts +58 -0
- package/scaffold/pm-api/src/routes/v2-retro.ts +221 -0
- package/scaffold/pm-api/src/routes/v2-rewards.ts +132 -0
- package/scaffold/pm-api/src/routes/v2-scenarios.ts +48 -0
- package/scaffold/pm-api/src/routes/v2-search.ts +32 -0
- package/scaffold/pm-api/src/routes/v2-standup.ts +127 -0
- package/scaffold/pm-api/src/routes/v2-user.ts +38 -0
- package/scaffold/pm-api/src/types.ts +11 -0
- package/scaffold/pm-api/src/utils/activity.ts +22 -0
- package/scaffold/pm-api/src/utils/admin.ts +9 -0
- package/scaffold/pm-api/src/utils/agent-notify.ts +62 -0
- package/scaffold/pm-api/src/utils/assignee.ts +69 -0
- package/scaffold/pm-api/src/utils/db.ts +45 -0
- package/scaffold/pm-api/src/utils/initiative.ts +23 -0
- package/scaffold/pm-api/src/utils/retro-link.ts +32 -0
- package/scaffold/pm-api/src/utils/sprint-lifecycle.ts +96 -0
- package/scaffold/pm-api/tsconfig.json +15 -0
- package/scaffold/pm-api/wrangler.toml.hbs +11 -0
- package/scaffold/spec-site/package-lock.json +892 -0
- package/scaffold/spec-site/package.json +15 -1
- package/scaffold/spec-site/src/api/types.ts +6 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +429 -55
- package/scaffold/spec-site/src/components/AuthGate.vue +117 -0
- package/scaffold/spec-site/src/components/BurndownChart.vue +78 -0
- package/scaffold/spec-site/src/components/DocComments.vue +137 -0
- package/scaffold/spec-site/src/components/DocEditor.vue +118 -0
- package/scaffold/spec-site/src/components/DocExportBar.vue +110 -0
- package/scaffold/spec-site/src/components/DocsSidebar.vue +309 -0
- package/scaffold/spec-site/src/components/EmptyState.vue +30 -0
- package/scaffold/spec-site/src/components/ErrorBanner.vue +38 -0
- package/scaffold/spec-site/src/components/Icon.vue +58 -0
- package/scaffold/spec-site/src/components/MemberSelect.vue +48 -0
- package/scaffold/spec-site/src/components/MemoChecklist.vue +88 -0
- package/scaffold/spec-site/src/components/MemoGraph.vue +75 -0
- package/scaffold/spec-site/src/components/MemoItem.vue +353 -0
- package/scaffold/spec-site/src/components/MemoRelations.vue +101 -0
- package/scaffold/spec-site/src/components/MemoTimeline.vue +53 -0
- package/scaffold/spec-site/src/components/MentionInput.vue +174 -0
- package/scaffold/spec-site/src/components/NotificationDropdown.vue +116 -0
- package/scaffold/spec-site/src/components/PriorityBadge.vue +23 -0
- package/scaffold/spec-site/src/components/SearchModal.vue +102 -0
- package/scaffold/spec-site/src/components/SlashCommand.ts +123 -0
- package/scaffold/spec-site/src/components/StateDisplay.vue +54 -0
- package/scaffold/spec-site/src/components/TreeNode.vue +82 -0
- package/scaffold/spec-site/src/components/UserAvatar.vue +24 -0
- package/scaffold/spec-site/src/components/VelocityChart.vue +77 -0
- package/scaffold/spec-site/src/composables/navTypes.ts +3 -0
- package/scaffold/spec-site/src/composables/pmTypes.ts +15 -2
- package/scaffold/spec-site/src/composables/useBottomSheet.ts +103 -0
- package/scaffold/spec-site/src/composables/useDashboard.ts +221 -0
- package/scaffold/spec-site/src/composables/useMediaQuery.ts +28 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +39 -0
- package/scaffold/spec-site/src/composables/useNotification.ts +200 -0
- package/scaffold/spec-site/src/composables/usePmStore.ts +48 -1
- package/scaffold/spec-site/src/composables/useRetro.ts +6 -0
- package/scaffold/spec-site/src/composables/useStandup.ts +201 -0
- package/scaffold/spec-site/src/composables/useTheme.ts +37 -0
- package/scaffold/spec-site/src/composables/useTurso.ts +17 -0
- package/scaffold/spec-site/src/composables/useUser.ts +19 -1
- package/scaffold/spec-site/src/composables/useViewport.ts +26 -0
- package/scaffold/spec-site/src/features.ts +108 -0
- package/scaffold/spec-site/src/mockup/ComponentPalette.vue +61 -0
- package/scaffold/spec-site/src/mockup/MockupCanvas.vue +459 -0
- package/scaffold/spec-site/src/mockup/PropertyPanel.vue +217 -0
- package/scaffold/spec-site/src/mockup/componentCatalog.ts +68 -0
- package/scaffold/spec-site/src/mockup/useScenarios.ts +67 -0
- package/scaffold/spec-site/src/pages/AdminPage.vue +299 -0
- package/scaffold/spec-site/src/pages/DashboardPage.vue +650 -0
- package/scaffold/spec-site/src/pages/DocsEditor.vue +119 -0
- package/scaffold/spec-site/src/pages/DocsHub.vue +157 -0
- package/scaffold/spec-site/src/pages/DocsPage.vue +444 -0
- package/scaffold/spec-site/src/pages/InboxPage.vue +156 -0
- package/scaffold/spec-site/src/pages/MeetingsPage.vue +294 -0
- package/scaffold/spec-site/src/pages/MemosPage.vue +857 -0
- package/scaffold/spec-site/src/pages/MockupEditorPage.vue +611 -0
- package/scaffold/spec-site/src/pages/MockupListPage.vue +121 -0
- package/scaffold/spec-site/src/pages/MockupViewerPage.vue +199 -0
- package/scaffold/spec-site/src/pages/MyPage.vue +343 -0
- package/scaffold/spec-site/src/pages/NotificationSettingsPage.vue +59 -0
- package/scaffold/spec-site/src/pages/RewardsPage.vue +266 -0
- package/scaffold/spec-site/src/pages/SprintAdmin.vue +521 -0
- package/scaffold/spec-site/src/pages/SprintTimeline.vue +159 -0
- package/scaffold/spec-site/src/pages/board/BoardAdmin.vue +422 -0
- package/scaffold/spec-site/src/pages/board/BoardEpicSection.vue +54 -0
- package/scaffold/spec-site/src/pages/board/BoardPage.vue +884 -0
- package/scaffold/spec-site/src/pages/board/BoardStoryCard.vue +67 -0
- package/scaffold/spec-site/src/pages/board/BoardTaskItem.vue +52 -0
- package/scaffold/spec-site/src/pages/board/KanbanBoard.vue +93 -0
- package/scaffold/spec-site/src/pages/board/MyTasksPage.vue +202 -0
- package/scaffold/spec-site/src/pages/board/SprintClose.vue +167 -0
- package/scaffold/spec-site/src/pages/board/SprintColumn.vue +49 -0
- package/scaffold/spec-site/src/pages/board/SprintKickoff.vue +389 -0
- package/scaffold/spec-site/src/pages/board/StatusBadge.vue +52 -0
- package/scaffold/spec-site/src/pages/board/StoryDetailPanel.vue +495 -0
- package/scaffold/spec-site/src/pages/board/TaskCard.vue +42 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +36 -2
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +82 -66
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +47 -18
- package/scaffold/spec-site/src/pages/standup/StandupEntryCard.vue +551 -0
- package/scaffold/spec-site/src/pages/standup/StandupForm.vue +68 -0
- package/scaffold/spec-site/src/pages/standup/StandupList.vue +71 -0
- package/scaffold/spec-site/src/pages/standup/StandupPage.vue +225 -0
- package/scaffold/spec-site/src/router.ts +141 -0
- package/scaffold/spec-site/src/styles/buttons.css +124 -0
- package/scaffold/spec-site/src/utils/parseMentions.ts +56 -0
- 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>
|