@startsimpli/ui 0.4.57 → 0.4.59
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/package.json +4 -3
- package/src/components/kanban/KanbanBoard.tsx +4 -0
- package/src/components/presentations/DeckBuilderWorkspace.test.tsx +114 -0
- package/src/components/presentations/DeckBuilderWorkspace.tsx +257 -0
- package/src/components/presentations/index.ts +13 -0
- package/src/components/presentations/types.ts +82 -0
- package/src/theme/tailwind.preset.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.59",
|
|
4
4
|
"description": "Shared UI components package for StartSimpli applications",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"./email-editor": "./src/components/email-editor/index.ts",
|
|
23
23
|
"./workflows": "./src/components/workflows/index.ts",
|
|
24
24
|
"./workflows/styles": "./src/components/workflows/workflows.css",
|
|
25
|
+
"./presentations": "./src/components/presentations/index.ts",
|
|
25
26
|
"./terminal/styles": "./src/components/terminal/xterm.css",
|
|
26
27
|
"./session": "./src/components/session/index.ts",
|
|
27
28
|
"./session-console": "./src/components/session/AgentSessionConsole.tsx",
|
|
@@ -41,8 +42,8 @@
|
|
|
41
42
|
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
42
43
|
"react": "^18.0.0 || ^19.0.0",
|
|
43
44
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
44
|
-
"@startsimpli/
|
|
45
|
-
"@startsimpli/
|
|
45
|
+
"@startsimpli/auth": "0.4.30",
|
|
46
|
+
"@startsimpli/api": "0.5.44"
|
|
46
47
|
},
|
|
47
48
|
"peerDependenciesMeta": {
|
|
48
49
|
"@startsimpli/api": {
|
|
@@ -155,6 +155,10 @@ export function KanbanBoard<T>({
|
|
|
155
155
|
ref={dp.innerRef}
|
|
156
156
|
{...dp.draggableProps}
|
|
157
157
|
{...dp.dragHandleProps}
|
|
158
|
+
// @hello-pangea/dnd's DraggableStyle isn't assignable to
|
|
159
|
+
// the radix-augmented CSSProperties; the spread already
|
|
160
|
+
// sets it, this re-asserts the type for consumer tsc.
|
|
161
|
+
style={dp.draggableProps.style as React.CSSProperties}
|
|
158
162
|
className={cn(ds.isDragging && 'opacity-80')}
|
|
159
163
|
>
|
|
160
164
|
{renderCard(item)}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
+
import { DeckBuilderWorkspace } from './DeckBuilderWorkspace'
|
|
5
|
+
import type { DeckBuilder } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The composer is pure: it renders whatever the injected `builder` exposes and
|
|
9
|
+
* lights up optional affordances (style chip, reference uploader) only when the
|
|
10
|
+
* builder declares them. These tests drive it with hand-rolled builders — no app
|
|
11
|
+
* import — proving the surface is app-agnostic.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function makeBuilder(overrides: Partial<DeckBuilder> = {}): DeckBuilder {
|
|
15
|
+
return {
|
|
16
|
+
deckTitle: '',
|
|
17
|
+
phase: 'empty',
|
|
18
|
+
messages: [],
|
|
19
|
+
slides: [],
|
|
20
|
+
setActiveId: jest.fn(),
|
|
21
|
+
sendMessage: jest.fn(),
|
|
22
|
+
regenerate: jest.fn(),
|
|
23
|
+
...overrides,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderWithClient(ui: React.ReactElement) {
|
|
28
|
+
const client = new QueryClient({
|
|
29
|
+
defaultOptions: { queries: { retry: false } },
|
|
30
|
+
})
|
|
31
|
+
return render(<QueryClientProvider client={client}>{ui}</QueryClientProvider>)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('DeckBuilderWorkspace', () => {
|
|
35
|
+
it('renders the dual-pane surface from a minimal injected builder', () => {
|
|
36
|
+
renderWithClient(<DeckBuilderWorkspace builder={makeBuilder()} />)
|
|
37
|
+
|
|
38
|
+
// Toolbar shows the placeholder title + the empty-outline hint.
|
|
39
|
+
expect(screen.getByText('New presentation')).toBeInTheDocument()
|
|
40
|
+
expect(screen.getByText('Paste an outline to begin')).toBeInTheDocument()
|
|
41
|
+
|
|
42
|
+
// Empty deck pane surfaces its empty-state copy.
|
|
43
|
+
expect(
|
|
44
|
+
screen.getByText(/Your slides will render here as a live deck/i)
|
|
45
|
+
).toBeInTheDocument()
|
|
46
|
+
|
|
47
|
+
// Phase-empty composer uses the outline placeholder.
|
|
48
|
+
expect(
|
|
49
|
+
screen.getByPlaceholderText(/Paste your outline/i)
|
|
50
|
+
).toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('routes composer submissions to builder.sendMessage', () => {
|
|
54
|
+
const builder = makeBuilder()
|
|
55
|
+
renderWithClient(<DeckBuilderWorkspace builder={builder} />)
|
|
56
|
+
|
|
57
|
+
const textarea = screen.getByPlaceholderText(/Paste your outline/i)
|
|
58
|
+
// Single-line content submits on bare Enter.
|
|
59
|
+
fireEvent.change(textarea, { target: { value: 'My deck outline' } })
|
|
60
|
+
fireEvent.keyDown(textarea, { key: 'Enter' })
|
|
61
|
+
|
|
62
|
+
expect(builder.sendMessage).toHaveBeenCalledWith('My deck outline')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('shows the derived-style chip only when the builder provides deckStyle', () => {
|
|
66
|
+
const { rerender } = renderWithClient(
|
|
67
|
+
<DeckBuilderWorkspace builder={makeBuilder()} />
|
|
68
|
+
)
|
|
69
|
+
expect(screen.queryByTestId('deck-style-chip')).not.toBeInTheDocument()
|
|
70
|
+
|
|
71
|
+
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
|
72
|
+
rerender(
|
|
73
|
+
<QueryClientProvider client={client}>
|
|
74
|
+
<DeckBuilderWorkspace
|
|
75
|
+
builder={makeBuilder({
|
|
76
|
+
deckStyle: { theme: 'Modern Professional', source: 'reference' },
|
|
77
|
+
})}
|
|
78
|
+
/>
|
|
79
|
+
</QueryClientProvider>
|
|
80
|
+
)
|
|
81
|
+
const chip = screen.getByTestId('deck-style-chip')
|
|
82
|
+
expect(chip).toBeInTheDocument()
|
|
83
|
+
expect(chip).toHaveTextContent('Modern Professional')
|
|
84
|
+
// `source: 'reference'` marks it as cloned.
|
|
85
|
+
expect(chip).toHaveTextContent(/cloned/i)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('wires the reference uploader only when the builder supports attachments', () => {
|
|
89
|
+
// Demo-style builder: no attachments capability -> no attach affordance.
|
|
90
|
+
const { unmount } = renderWithClient(
|
|
91
|
+
<DeckBuilderWorkspace builder={makeBuilder()} />
|
|
92
|
+
)
|
|
93
|
+
expect(screen.queryByTestId('chat-attach-input')).not.toBeInTheDocument()
|
|
94
|
+
unmount()
|
|
95
|
+
|
|
96
|
+
// Backend-style builder: supportsAttachments + an injected client -> uploader on.
|
|
97
|
+
const attachments = {
|
|
98
|
+
listAttachments: jest.fn().mockResolvedValue([]),
|
|
99
|
+
uploadAttachment: jest.fn(),
|
|
100
|
+
deleteAttachment: jest.fn(),
|
|
101
|
+
}
|
|
102
|
+
renderWithClient(
|
|
103
|
+
<DeckBuilderWorkspace
|
|
104
|
+
builder={makeBuilder({
|
|
105
|
+
supportsAttachments: true,
|
|
106
|
+
documentId: 'doc-1',
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
attachments: attachments as any,
|
|
109
|
+
})}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
expect(screen.getByTestId('chat-attach-input')).toBeInTheDocument()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { ArrowLeft, Download, FileText, Palette, Presentation, Sparkles } from 'lucide-react'
|
|
6
|
+
import { useQuery } from '@tanstack/react-query'
|
|
7
|
+
import type { Attachment } from '@startsimpli/api'
|
|
8
|
+
import { DualPaneWorkspace } from '../workspace'
|
|
9
|
+
import { ChatThread, ChatComposer } from '../chat'
|
|
10
|
+
import type { ChatMessageData, ChatMessageAttachment } from '../chat/types'
|
|
11
|
+
import { SlideDeckViewer } from '../slide-deck'
|
|
12
|
+
import type { DeckBuilder } from './types'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reopen display: if no message already carries attachments (the conversation
|
|
16
|
+
* payload didn't echo them), hang the document's persisted reference attachments
|
|
17
|
+
* off the earliest user message so they reappear in the chat after reopen.
|
|
18
|
+
*/
|
|
19
|
+
function hydratePriorAttachments(
|
|
20
|
+
messages: ChatMessageData[],
|
|
21
|
+
prior: Attachment[] | undefined
|
|
22
|
+
): ChatMessageData[] {
|
|
23
|
+
if (!prior || prior.length === 0) return messages
|
|
24
|
+
if (messages.some((m) => m.attachments && m.attachments.length > 0)) return messages
|
|
25
|
+
const targetIdx = messages.findIndex((m) => m.role === 'user')
|
|
26
|
+
if (targetIdx === -1) return messages
|
|
27
|
+
const chips: ChatMessageAttachment[] = prior
|
|
28
|
+
.filter((a) => a.purpose === 'reference')
|
|
29
|
+
.map((a) => ({
|
|
30
|
+
id: a.id,
|
|
31
|
+
fileUrl: a.fileUrl,
|
|
32
|
+
filename: a.filename,
|
|
33
|
+
contentType: a.contentType,
|
|
34
|
+
}))
|
|
35
|
+
if (chips.length === 0) return messages
|
|
36
|
+
return messages.map((m, i) => (i === targetIdx ? { ...m, attachments: chips } : m))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DeckBuilderWorkspaceProps {
|
|
40
|
+
/** Where the back arrow points. Omit to hide it (e.g. on a public /try page). */
|
|
41
|
+
backHref?: string
|
|
42
|
+
/**
|
|
43
|
+
* The orchestrator to render — injected, never imported here. The consuming
|
|
44
|
+
* app wires a demo/no-backend engine or the backend workflow-run consumer;
|
|
45
|
+
* both expose the identical `DeckBuilder` contract. Reference-attachment
|
|
46
|
+
* support (uploader + reopen hydration) rides on `builder.attachments`.
|
|
47
|
+
*/
|
|
48
|
+
builder: DeckBuilder
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The chat | slides deck-builder page composer. Composed entirely from generic
|
|
53
|
+
* @startsimpli/ui primitives (`DualPaneWorkspace` + chat + `SlideDeckViewer`)
|
|
54
|
+
* wired to an injected deck-builder orchestrator.
|
|
55
|
+
*
|
|
56
|
+
* Pure + app-agnostic: the orchestrator is a prop (`builder`) so the same
|
|
57
|
+
* surface can be driven by a demo engine or a backend workflow-run consumer,
|
|
58
|
+
* and the reference-attachments client rides on the builder — the app renders
|
|
59
|
+
* this as a thin `<DeckBuilderWorkspace builder={...} />` wrapper.
|
|
60
|
+
*/
|
|
61
|
+
export function DeckBuilderWorkspace({ backHref, builder }: DeckBuilderWorkspaceProps) {
|
|
62
|
+
const {
|
|
63
|
+
deckTitle,
|
|
64
|
+
phase,
|
|
65
|
+
messages,
|
|
66
|
+
slides,
|
|
67
|
+
activeId,
|
|
68
|
+
pptxUrl,
|
|
69
|
+
pdfUrl,
|
|
70
|
+
isWorking,
|
|
71
|
+
documentId,
|
|
72
|
+
deckStyle,
|
|
73
|
+
attachments,
|
|
74
|
+
setActiveId,
|
|
75
|
+
sendMessage,
|
|
76
|
+
regenerate,
|
|
77
|
+
deleteSlide,
|
|
78
|
+
moveSlide,
|
|
79
|
+
insertSlide,
|
|
80
|
+
editSlide,
|
|
81
|
+
reorderSlide,
|
|
82
|
+
} = builder
|
|
83
|
+
|
|
84
|
+
// Only a backend-backed engine has a Document for reference uploads to land
|
|
85
|
+
// on AND an attachments client wired up; a demo engine would strand every file
|
|
86
|
+
// as a forever-queued chip, so it gets no uploader (attach affordance hidden).
|
|
87
|
+
const canAttach = builder.supportsAttachments === true && !!attachments
|
|
88
|
+
|
|
89
|
+
// A turn is in progress (agent thinking / slides generating). Prefer the
|
|
90
|
+
// builder's own signal; fall back to slide status for engines that omit it.
|
|
91
|
+
const busy = isWorking ?? slides.some((s) => s.status === 'generating')
|
|
92
|
+
|
|
93
|
+
// Reference attachments are bundled into the shared ChatComposer: pass it the
|
|
94
|
+
// document-scoped uploader and the composer self-manages upload state (chips,
|
|
95
|
+
// downscale, retry, remove). Uploads are document-scoped — for a brand-new deck
|
|
96
|
+
// with no document yet, the composer holds files client-side and flushes them
|
|
97
|
+
// the moment the first turn creates the document and `documentId` arrives. The
|
|
98
|
+
// backend agent gathers references off the Document, so we do NOT thread
|
|
99
|
+
// attachment ids through the turn — landing the upload on `documentId` is enough.
|
|
100
|
+
|
|
101
|
+
// Reopen: surface the document's persisted reference attachments. Fetched off
|
|
102
|
+
// the injected attachments client (mirrors @startsimpli/hooks `useAttachments`
|
|
103
|
+
// — the surface takes react-query as a peer, so keep the query inline rather
|
|
104
|
+
// than couple this package to @startsimpli/hooks). Disabled until a real
|
|
105
|
+
// documentId + client exist. Hydrated onto the earliest user message when the
|
|
106
|
+
// conversation payload didn't already carry them, so they reappear on reopen.
|
|
107
|
+
const { data: priorAttachments } = useQuery({
|
|
108
|
+
queryKey: ['attachments', 'list', documentId ?? ''],
|
|
109
|
+
queryFn: () => attachments!.listAttachments(documentId!),
|
|
110
|
+
enabled: !!attachments && !!documentId && documentId !== 'new',
|
|
111
|
+
})
|
|
112
|
+
const messagesWithAttachments = React.useMemo(
|
|
113
|
+
() => hydratePriorAttachments(messages, priorAttachments),
|
|
114
|
+
[messages, priorAttachments]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
// Compact display of the deck's derived style: theme + any non-"auto" palette/
|
|
118
|
+
// font. "auto" is the placeholder default, so it's dropped to avoid noise.
|
|
119
|
+
// Style values can be anything from a short label ("Modern Professional") to a
|
|
120
|
+
// multi-sentence prose spec (when cloned from a reference image via vision), so
|
|
121
|
+
// each bit is hard-truncated to a glanceable phrase and the chip is width-
|
|
122
|
+
// capped + single-line — the full spec lives in the hover title.
|
|
123
|
+
const shortBit = (v: string) => {
|
|
124
|
+
const firstPhrase = v.split(/[.;,—]/)[0].trim()
|
|
125
|
+
const s = (firstPhrase || v).trim()
|
|
126
|
+
return s.length > 28 ? `${s.slice(0, 28).trimEnd()}…` : s
|
|
127
|
+
}
|
|
128
|
+
const rawBits = deckStyle
|
|
129
|
+
? [deckStyle.theme, deckStyle.palette, deckStyle.font]
|
|
130
|
+
.filter((v): v is string => !!v && v !== 'auto')
|
|
131
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
132
|
+
: []
|
|
133
|
+
const styleLabel = rawBits.map(shortBit).join(' · ')
|
|
134
|
+
const styleTitle = [deckStyle?.theme, deckStyle?.palette, deckStyle?.font, deckStyle?.mood]
|
|
135
|
+
.filter((v): v is string => !!v && v !== 'auto')
|
|
136
|
+
.join('\n\n')
|
|
137
|
+
const styleChip = deckStyle && (rawBits.length > 0 || deckStyle.source === 'reference') && (
|
|
138
|
+
<div
|
|
139
|
+
className="hidden max-w-[280px] items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-gray-600 sm:inline-flex"
|
|
140
|
+
data-testid="deck-style-chip"
|
|
141
|
+
title={styleTitle || 'Deck style'}
|
|
142
|
+
>
|
|
143
|
+
{deckStyle.source === 'reference' ? (
|
|
144
|
+
<Sparkles className="h-3.5 w-3.5 shrink-0 text-primary-600" aria-hidden="true" />
|
|
145
|
+
) : (
|
|
146
|
+
<Palette className="h-3.5 w-3.5 shrink-0 text-gray-400" aria-hidden="true" />
|
|
147
|
+
)}
|
|
148
|
+
<span className="truncate font-medium capitalize">{styleLabel || 'Custom style'}</span>
|
|
149
|
+
{deckStyle.source === 'reference' && (
|
|
150
|
+
<span className="shrink-0 text-primary-600">· cloned</span>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
const toolbar = (
|
|
156
|
+
<div className="flex items-center justify-between gap-3 bg-white px-4 py-2">
|
|
157
|
+
<div className="flex items-center gap-3">
|
|
158
|
+
{backHref && (
|
|
159
|
+
<Link
|
|
160
|
+
href={backHref}
|
|
161
|
+
className="rounded p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-700"
|
|
162
|
+
aria-label="Back"
|
|
163
|
+
>
|
|
164
|
+
<ArrowLeft className="h-4 w-4" />
|
|
165
|
+
</Link>
|
|
166
|
+
)}
|
|
167
|
+
<Presentation className="h-5 w-5 text-primary-600" aria-hidden="true" />
|
|
168
|
+
<div className="leading-tight">
|
|
169
|
+
<div className="text-sm font-semibold text-gray-900">{deckTitle || 'New presentation'}</div>
|
|
170
|
+
<div className="text-xs text-gray-400">
|
|
171
|
+
{slides.length > 0 ? `${slides.length} slides · ${phase}` : 'Paste an outline to begin'}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<div className="flex items-center gap-2">
|
|
176
|
+
{styleChip}
|
|
177
|
+
{pptxUrl && (
|
|
178
|
+
<a
|
|
179
|
+
href={pptxUrl}
|
|
180
|
+
download={`${deckTitle || 'deck'}.pptx.txt`}
|
|
181
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-sm shadow-sm hover:bg-accent"
|
|
182
|
+
>
|
|
183
|
+
<Download className="h-3.5 w-3.5" /> PPTX
|
|
184
|
+
</a>
|
|
185
|
+
)}
|
|
186
|
+
{pdfUrl && (
|
|
187
|
+
<a
|
|
188
|
+
href={pdfUrl}
|
|
189
|
+
download={`${deckTitle || 'deck'}.pdf.txt`}
|
|
190
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-sm shadow-sm hover:bg-accent"
|
|
191
|
+
>
|
|
192
|
+
<FileText className="h-3.5 w-3.5" /> PDF
|
|
193
|
+
</a>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const chat = (
|
|
200
|
+
<div className="flex h-full min-h-0 flex-col bg-white">
|
|
201
|
+
<ChatThread
|
|
202
|
+
messages={messagesWithAttachments}
|
|
203
|
+
busy={busy}
|
|
204
|
+
busyLabel="Working on your deck…"
|
|
205
|
+
className="flex-1"
|
|
206
|
+
emptyState={<span className="text-sm text-gray-400">Your conversation will appear here.</span>}
|
|
207
|
+
/>
|
|
208
|
+
<ChatComposer
|
|
209
|
+
onSubmit={sendMessage}
|
|
210
|
+
busy={busy}
|
|
211
|
+
uploader={canAttach ? attachments : undefined}
|
|
212
|
+
documentId={documentId}
|
|
213
|
+
placeholder={
|
|
214
|
+
phase === 'empty'
|
|
215
|
+
? 'Paste your outline (title on line 1, then slides)…'
|
|
216
|
+
: 'Reply — “looks good”, “redo slide 2 …”, “compile”…'
|
|
217
|
+
}
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
const deck = (
|
|
223
|
+
<SlideDeckViewer
|
|
224
|
+
slides={slides}
|
|
225
|
+
activeId={activeId}
|
|
226
|
+
onActiveChange={setActiveId}
|
|
227
|
+
onRegenerate={regenerate}
|
|
228
|
+
onDelete={deleteSlide}
|
|
229
|
+
onMove={moveSlide}
|
|
230
|
+
onInsert={insertSlide}
|
|
231
|
+
onEdit={editSlide}
|
|
232
|
+
onReorder={reorderSlide}
|
|
233
|
+
emptyState={
|
|
234
|
+
<div className="max-w-sm">
|
|
235
|
+
<Presentation className="mx-auto mb-3 h-10 w-10 text-gray-300" aria-hidden="true" />
|
|
236
|
+
<p className="text-sm text-gray-500">
|
|
237
|
+
Your slides will render here as a live deck — paste an outline in the chat to start.
|
|
238
|
+
</p>
|
|
239
|
+
</div>
|
|
240
|
+
}
|
|
241
|
+
/>
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<DualPaneWorkspace
|
|
246
|
+
toolbar={toolbar}
|
|
247
|
+
leftLabel="Chat"
|
|
248
|
+
rightLabel="Slides"
|
|
249
|
+
left={chat}
|
|
250
|
+
right={deck}
|
|
251
|
+
collapsible
|
|
252
|
+
initialLeftSize={38}
|
|
253
|
+
storageKey="present-web:builder-split"
|
|
254
|
+
className="h-full"
|
|
255
|
+
/>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @startsimpli/ui/presentations — the deck-builder page composer.
|
|
3
|
+
*
|
|
4
|
+
* A dual-pane (chat | live slide deck) surface composed entirely from generic
|
|
5
|
+
* @startsimpli/ui primitives and driven by an injected orchestrator that
|
|
6
|
+
* satisfies the `DeckBuilder` contract. Isolated behind a dedicated subpath so
|
|
7
|
+
* consumers that don't build presentations don't pull it in (mirrors gantt /
|
|
8
|
+
* workflows / collection).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { DeckBuilderWorkspace } from './DeckBuilderWorkspace'
|
|
12
|
+
export type { DeckBuilderWorkspaceProps } from './DeckBuilderWorkspace'
|
|
13
|
+
export type { DeckBuilder, DeckStyle, DeckBuilderPhase } from './types'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { AttachmentsApi } from '@startsimpli/api'
|
|
2
|
+
import type { ChatMessageData } from '../chat/types'
|
|
3
|
+
import type { SlideData } from '../slide-deck/types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @startsimpli/ui/presentations — the deck-builder page composer contract.
|
|
7
|
+
*
|
|
8
|
+
* `DeckBuilder` is the pure view-model interface the `DeckBuilderWorkspace`
|
|
9
|
+
* composer consumes. It is deliberately engine-agnostic: any orchestrator that
|
|
10
|
+
* satisfies this shape (a demo/no-backend engine, or the backend workflow-run
|
|
11
|
+
* consumer) can drive the same dual-pane surface. The concrete orchestrators
|
|
12
|
+
* live in the consuming app (present-web); this package owns only the surface +
|
|
13
|
+
* the contract it renders against.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type DeckBuilderPhase = 'empty' | 'review_first' | 'walking' | 'ready'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The deck's ONE derived style spec, surfaced for display on the toolbar
|
|
20
|
+
* (theme/palette/font/mood). `source: 'reference'` marks a style cloned from an
|
|
21
|
+
* uploaded reference image (the design-cloning path). Display-only — the
|
|
22
|
+
* orchestrator owns styling.
|
|
23
|
+
*/
|
|
24
|
+
export interface DeckStyle {
|
|
25
|
+
theme?: string
|
|
26
|
+
palette?: string
|
|
27
|
+
font?: string
|
|
28
|
+
mood?: string
|
|
29
|
+
source?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The orchestrator contract driving the dual-pane deck builder. Required fields
|
|
34
|
+
* are what every engine must provide; the rest are optional capabilities the
|
|
35
|
+
* surface lights up when present (per-slide actions, export urls, reference
|
|
36
|
+
* attachments, a derived style chip).
|
|
37
|
+
*/
|
|
38
|
+
export interface DeckBuilder {
|
|
39
|
+
deckTitle: string
|
|
40
|
+
phase: DeckBuilderPhase
|
|
41
|
+
messages: ChatMessageData[]
|
|
42
|
+
slides: SlideData[]
|
|
43
|
+
activeId?: string
|
|
44
|
+
pptxUrl?: string
|
|
45
|
+
pdfUrl?: string
|
|
46
|
+
/** True while a turn is being processed (agent thinking / slides generating). */
|
|
47
|
+
isWorking?: boolean
|
|
48
|
+
/** The backend-derived style spec, surfaced for a compact toolbar chip. */
|
|
49
|
+
deckStyle?: DeckStyle
|
|
50
|
+
/**
|
|
51
|
+
* True when this engine has a real backend document that reference uploads can
|
|
52
|
+
* land on. A local/demo engine omits it, so the composer suppresses the attach
|
|
53
|
+
* affordance and never strands a file as a forever-queued chip.
|
|
54
|
+
*/
|
|
55
|
+
supportsAttachments?: boolean
|
|
56
|
+
/**
|
|
57
|
+
* The active document id (the deck being edited), once one exists. Drives
|
|
58
|
+
* document-scoped reference-attachment uploads + reopen hydration. Undefined
|
|
59
|
+
* for a brand-new deck until the first turn creates it, and for a demo engine.
|
|
60
|
+
*/
|
|
61
|
+
documentId?: string
|
|
62
|
+
/**
|
|
63
|
+
* Reference-attachments client (e.g. `@startsimpli/api` `api.attachments`).
|
|
64
|
+
* Ride it on the builder so the composer stays a `{ builder }`-only surface:
|
|
65
|
+
* the composer wires it as the chat composer's uploader and reads persisted
|
|
66
|
+
* references off it on reopen. Omit on engines without a backend document.
|
|
67
|
+
*/
|
|
68
|
+
attachments?: AttachmentsApi
|
|
69
|
+
setActiveId: (id: string) => void
|
|
70
|
+
sendMessage: (text: string) => void
|
|
71
|
+
regenerate: (slide: SlideData) => void
|
|
72
|
+
/** Delete a slide (dispatched as an agent turn). Optional — demo engine omits it. */
|
|
73
|
+
deleteSlide?: (slide: SlideData) => void
|
|
74
|
+
/** Move a slide one position left/right (dispatched as an agent turn). Optional. */
|
|
75
|
+
moveSlide?: (slide: SlideData, direction: 'left' | 'right') => void
|
|
76
|
+
/** Insert a blank slide after the given slide (dispatched as an agent turn). Optional. */
|
|
77
|
+
insertSlide?: (afterSlide: SlideData) => void
|
|
78
|
+
/** Edit a slide's title/content then regenerate it (dispatched as an agent turn). Optional. */
|
|
79
|
+
editSlide?: (slide: SlideData, edits: { title?: string; content?: string }) => void
|
|
80
|
+
/** Drag-reorder: move the slide at fromIndex to toIndex (0-based; agent turn). Optional. */
|
|
81
|
+
reorderSlide?: (fromIndex: number, toIndex: number) => void
|
|
82
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { Config } from 'tailwindcss'
|
|
|
3
3
|
// Presets intentionally omit `content` — each consuming app supplies its own.
|
|
4
4
|
// tailwindcss exposes PresetsConfig = Partial<Config> for this purpose.
|
|
5
5
|
const preset: Partial<Config> = {
|
|
6
|
-
darkMode:
|
|
6
|
+
darkMode: 'class',
|
|
7
7
|
theme: {
|
|
8
8
|
extend: {
|
|
9
9
|
colors: {
|