working llm util
This commit is contained in:
parent
fdb96b6ae1
commit
7805a1e961
|
|
@ -9,7 +9,8 @@
|
|||
"dev:worker": "wrangler dev --local --port 5172 --ip 0.0.0.0",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"deploy": "tsc && vite build && vercel deploy --prod && wrangler deploy"
|
||||
"deploy": "tsc && vite build && vercel deploy --prod && wrangler deploy",
|
||||
"types": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Jeff Emmett",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useSync } from "@tldraw/sync"
|
||||
import { useMemo } from "react"
|
||||
import { Tldraw, Editor, useTools, useIsToolSelected, DefaultToolbar, TldrawUiMenuItem, DefaultToolbarContent, DefaultKeyboardShortcutsDialog, DefaultKeyboardShortcutsDialogContent, defaultTools } from "tldraw"
|
||||
import { useMemo, useEffect, useState } from "react"
|
||||
import { Tldraw, Editor } from "tldraw"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { ChatBoxTool } from "@/tools/ChatBoxTool"
|
||||
import { ChatBoxShape } from "@/shapes/ChatBoxShapeUtil"
|
||||
|
|
@ -12,130 +12,47 @@ import { EmbedTool } from "@/tools/EmbedTool"
|
|||
import { MarkdownShape } from "@/shapes/MarkdownShapeUtil"
|
||||
import { MarkdownTool } from "@/tools/MarkdownTool"
|
||||
import { defaultShapeUtils, defaultBindingUtils } from "tldraw"
|
||||
import { useState } from "react"
|
||||
import { components } from "@/ui/components"
|
||||
import { overrides } from "@/ui/overrides"
|
||||
import { unfurlBookmarkUrl } from "../utils/unfurlBookmarkUrl"
|
||||
import { handleInitialPageLoad } from "@/utils/handleInitialPageLoad"
|
||||
import { MycrozineTemplateTool } from "@/tools/MycrozineTemplateTool"
|
||||
import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil"
|
||||
import { registerPropagators, ChangePropagator, TickPropagator, ClickPropagator } from "@/propagators/ScopedPropagators"
|
||||
import {
|
||||
registerPropagators,
|
||||
ChangePropagator,
|
||||
TickPropagator,
|
||||
ClickPropagator,
|
||||
} from "@/propagators/ScopedPropagators"
|
||||
import { SlideShapeTool } from "@/tools/SlideShapeTool"
|
||||
import { ISlideShape, SlideShapeUtil } from "@/shapes/SlideShapeUtil"
|
||||
import { SlidesPanel } from "@/slides/SlidesPanel"
|
||||
import { moveToSlide } from "@/slides/useSlides"
|
||||
import { SlideShapeUtil } from "@/shapes/SlideShapeUtil"
|
||||
import { makeRealSettings, applySettingsMigrations } from "@/lib/settings"
|
||||
import { PromptShapeTool } from "@/tools/PromptShapeTool"
|
||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||
import { llm } from "@/utils/llm"
|
||||
|
||||
// Default to production URL if env var isn't available
|
||||
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
||||
|
||||
const updatedComponents = {
|
||||
...components,
|
||||
HelperButtons: SlidesPanel,
|
||||
Minimap: null,
|
||||
Toolbar: (props: any) => {
|
||||
const tools = useTools()
|
||||
const slideTool = tools['Slide']
|
||||
const isSlideSelected = slideTool ? useIsToolSelected(slideTool) : false
|
||||
return (
|
||||
<DefaultToolbar {...props}>
|
||||
{slideTool && <TldrawUiMenuItem {...slideTool} isSelected={isSlideSelected} />}
|
||||
<DefaultToolbarContent />
|
||||
</DefaultToolbar>
|
||||
)
|
||||
},
|
||||
KeyboardShortcutsDialog: (props: any) => {
|
||||
const tools = useTools()
|
||||
return (
|
||||
<DefaultKeyboardShortcutsDialog {...props}>
|
||||
<TldrawUiMenuItem {...tools['Slide']} />
|
||||
<DefaultKeyboardShortcutsDialogContent />
|
||||
</DefaultKeyboardShortcutsDialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const customShapeUtils = [
|
||||
ChatBoxShape,
|
||||
VideoChatShape,
|
||||
EmbedShape,
|
||||
ChatBoxShape,
|
||||
VideoChatShape,
|
||||
EmbedShape,
|
||||
SlideShapeUtil,
|
||||
MycrozineTemplateShape,
|
||||
MarkdownShape
|
||||
MycrozineTemplateShape,
|
||||
MarkdownShape,
|
||||
PromptShape,
|
||||
]
|
||||
const customTools = [
|
||||
ChatBoxTool,
|
||||
VideoChatTool,
|
||||
EmbedTool,
|
||||
ChatBoxTool,
|
||||
VideoChatTool,
|
||||
EmbedTool,
|
||||
SlideShapeTool,
|
||||
MycrozineTemplateTool,
|
||||
MarkdownTool
|
||||
MycrozineTemplateTool,
|
||||
MarkdownTool,
|
||||
PromptShapeTool,
|
||||
]
|
||||
|
||||
const updatedOverrides = {
|
||||
...overrides,
|
||||
actions(editor: Editor, actions: any) {
|
||||
return {
|
||||
...actions,
|
||||
'next-slide': {
|
||||
id: 'next-slide',
|
||||
label: 'Next slide',
|
||||
kbd: 'right',
|
||||
onSelect() {
|
||||
const slides = editor.getCurrentPageShapes().filter(shape => shape.type === 'Slide')
|
||||
if (slides.length === 0) return
|
||||
|
||||
const currentSlide = editor.getSelectedShapes().find(shape => shape.type === 'Slide')
|
||||
const currentIndex = currentSlide
|
||||
? slides.findIndex(slide => slide.id === currentSlide.id)
|
||||
: -1
|
||||
|
||||
console.log('Current index:', currentIndex)
|
||||
console.log('Current slide:', currentSlide)
|
||||
|
||||
|
||||
// Calculate next index with wraparound
|
||||
const nextIndex = currentIndex === -1
|
||||
? 0
|
||||
: currentIndex >= slides.length - 1
|
||||
? 0
|
||||
: currentIndex + 1
|
||||
|
||||
const nextSlide = slides[nextIndex]
|
||||
|
||||
editor.select(nextSlide.id)
|
||||
editor.stopCameraAnimation()
|
||||
moveToSlide(editor, nextSlide as ISlideShape)
|
||||
},
|
||||
},
|
||||
'previous-slide': {
|
||||
id: 'previous-slide',
|
||||
label: 'Previous slide',
|
||||
kbd: 'left',
|
||||
onSelect() {
|
||||
const slides = editor.getCurrentPageShapes().filter(shape => shape.type === 'Slide')
|
||||
if (slides.length === 0) return
|
||||
|
||||
const currentSlide = editor.getSelectedShapes().find(shape => shape.type === 'Slide')
|
||||
const currentIndex = currentSlide
|
||||
? slides.findIndex(slide => slide.id === currentSlide.id)
|
||||
: -1
|
||||
|
||||
// Calculate previous index with wraparound
|
||||
const previousIndex = currentIndex <= 0
|
||||
? slides.length - 1
|
||||
: currentIndex - 1
|
||||
|
||||
const previousSlide = slides[previousIndex]
|
||||
|
||||
editor.select(previousSlide.id)
|
||||
editor.stopCameraAnimation()
|
||||
moveToSlide(editor, previousSlide as ISlideShape)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export function Board() {
|
||||
const { slug } = useParams<{ slug: string }>()
|
||||
const roomId = slug || "default-room"
|
||||
|
|
@ -153,8 +70,18 @@ export function Board() {
|
|||
const store = useSync(storeConfig)
|
||||
const [editor, setEditor] = useState<Editor | null>(null)
|
||||
|
||||
//console.log("store:", store)
|
||||
//console.log("store.store:",store.store)
|
||||
useEffect(() => {
|
||||
const value = localStorage.getItem("makereal_settings_2")
|
||||
if (value) {
|
||||
const json = JSON.parse(value)
|
||||
const migratedSettings = applySettingsMigrations(json)
|
||||
localStorage.setItem(
|
||||
"makereal_settings_2",
|
||||
JSON.stringify(migratedSettings),
|
||||
)
|
||||
makeRealSettings.set(migratedSettings)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ position: "fixed", inset: 0 }}>
|
||||
|
|
@ -162,11 +89,11 @@ export function Board() {
|
|||
store={store.store}
|
||||
shapeUtils={customShapeUtils}
|
||||
tools={customTools}
|
||||
components={updatedComponents}
|
||||
overrides={updatedOverrides}
|
||||
components={components}
|
||||
overrides={overrides}
|
||||
cameraOptions={{
|
||||
zoomSteps: [
|
||||
0.001, // Min zoom
|
||||
0.001, // Min zoom
|
||||
0.0025,
|
||||
0.005,
|
||||
0.01,
|
||||
|
|
@ -181,15 +108,26 @@ export function Board() {
|
|||
8,
|
||||
16,
|
||||
32,
|
||||
64 // Max zoom
|
||||
]
|
||||
64, // Max zoom
|
||||
],
|
||||
}}
|
||||
onMount={(editor) => {
|
||||
setEditor(editor)
|
||||
editor.registerExternalAssetHandler("url", unfurlBookmarkUrl)
|
||||
editor.setCurrentTool("hand")
|
||||
handleInitialPageLoad(editor)
|
||||
registerPropagators(editor, [TickPropagator,ChangePropagator,ClickPropagator])
|
||||
registerPropagators(editor, [
|
||||
TickPropagator,
|
||||
ChangePropagator,
|
||||
ClickPropagator,
|
||||
])
|
||||
llm(
|
||||
"You are a helpful assistant.",
|
||||
"Hello, how are you?",
|
||||
(partialResponse, done) => {
|
||||
console.log({ partialResponse, done })
|
||||
},
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
import {
|
||||
BaseBoxShapeUtil,
|
||||
HTMLContainer,
|
||||
TLBaseShape,
|
||||
TLGeoShape,
|
||||
TLShape,
|
||||
} from "tldraw"
|
||||
import { getEdge } from "@/propagators/tlgraph"
|
||||
import { llm } from "@/utils/llm"
|
||||
import { isShapeOfType } from "@/propagators/utils"
|
||||
|
||||
type IPrompt = TLBaseShape<
|
||||
"prompt",
|
||||
{
|
||||
w: number
|
||||
h: number
|
||||
prompt: string
|
||||
value: string
|
||||
agentBinding: string | null
|
||||
}
|
||||
>
|
||||
|
||||
export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
|
||||
static override type = "prompt" as const
|
||||
|
||||
FIXED_HEIGHT = 50 as const
|
||||
MIN_WIDTH = 150 as const
|
||||
PADDING = 4 as const
|
||||
|
||||
getDefaultProps(): IPrompt["props"] {
|
||||
return {
|
||||
w: 300,
|
||||
h: 50,
|
||||
prompt: "",
|
||||
value: "",
|
||||
agentBinding: null,
|
||||
}
|
||||
}
|
||||
|
||||
// override onResize: TLResizeHandle<IPrompt> = (
|
||||
// shape,
|
||||
// { scaleX, initialShape },
|
||||
// ) => {
|
||||
// const { x, y } = shape
|
||||
// const w = initialShape.props.w * scaleX
|
||||
// return {
|
||||
// x,
|
||||
// y,
|
||||
// props: {
|
||||
// ...shape.props,
|
||||
// w: Math.max(Math.abs(w), this.MIN_WIDTH),
|
||||
// h: this.FIXED_HEIGHT,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
component(shape: IPrompt) {
|
||||
const arrowBindings = this.editor.getBindingsInvolvingShape(
|
||||
shape.id,
|
||||
"arrow",
|
||||
)
|
||||
const arrows = arrowBindings
|
||||
.map((binding) => this.editor.getShape(binding.fromId))
|
||||
|
||||
const inputMap = arrows.reduce((acc, arrow) => {
|
||||
const edge = getEdge(arrow, this.editor);
|
||||
if (edge) {
|
||||
const sourceShape = this.editor.getShape(edge.from);
|
||||
if (sourceShape && edge.text) {
|
||||
acc[edge.text] = sourceShape;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, TLShape>);
|
||||
|
||||
const generateText = async (prompt: string) => {
|
||||
await llm('', prompt, (partial: string, done: boolean) => {
|
||||
console.log("DONE??", done)
|
||||
this.editor.updateShape<IPrompt>({
|
||||
id: shape.id,
|
||||
type: "prompt",
|
||||
props: { value: partial, agentBinding: done ? null : 'someone' },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handlePrompt = () => {
|
||||
if (shape.props.agentBinding) {
|
||||
return
|
||||
}
|
||||
let processedPrompt = shape.props.prompt;
|
||||
for (const [key, sourceShape] of Object.entries(inputMap)) {
|
||||
const pattern = `{${key}}`;
|
||||
if (processedPrompt.includes(pattern)) {
|
||||
if (isShapeOfType<TLGeoShape>(sourceShape, 'geo')) {
|
||||
processedPrompt = processedPrompt.replace(pattern, sourceShape.props.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(processedPrompt);
|
||||
generateText(processedPrompt)
|
||||
};
|
||||
|
||||
return (
|
||||
<HTMLContainer
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
border: "1px solid lightgrey",
|
||||
padding: this.PADDING,
|
||||
height: this.FIXED_HEIGHT,
|
||||
width: shape.props.w,
|
||||
pointerEvents: "all",
|
||||
backgroundColor: "#efefef",
|
||||
overflow: "visible",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
outline: shape.props.agentBinding ? "2px solid orange" : "none",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "visible",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.05)",
|
||||
border: "1px solid rgba(0, 0, 0, 0.05)",
|
||||
borderRadius: 6 - this.PADDING,
|
||||
fontSize: 16,
|
||||
}}
|
||||
type="text"
|
||||
placeholder="Enter prompt..."
|
||||
value={shape.props.prompt}
|
||||
onChange={(text) => {
|
||||
this.editor.updateShape<IPrompt>({
|
||||
id: shape.id,
|
||||
type: "prompt",
|
||||
props: { prompt: text.target.value },
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
style={{
|
||||
width: 100,
|
||||
height: "100%",
|
||||
marginLeft: 5,
|
||||
pointerEvents: "all",
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
type="button"
|
||||
onClick={handlePrompt}
|
||||
>
|
||||
Prompt
|
||||
</button>
|
||||
</HTMLContainer>
|
||||
)
|
||||
}
|
||||
|
||||
// [5]
|
||||
indicator(shape: IPrompt) {
|
||||
return <rect width={shape.props.w} height={shape.props.h} rx={5} />
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { BaseBoxShapeTool } from 'tldraw'
|
||||
|
||||
export class PromptShapeTool extends BaseBoxShapeTool {
|
||||
static override id = 'prompt'
|
||||
static override initial = 'idle'
|
||||
override shapeType = 'prompt'
|
||||
|
||||
}
|
||||
|
|
@ -1,198 +1,47 @@
|
|||
import {
|
||||
TLUiDialogProps,
|
||||
TldrawUiButton,
|
||||
TldrawUiButtonLabel,
|
||||
TldrawUiDialogBody,
|
||||
TldrawUiDialogCloseButton,
|
||||
TldrawUiDialogFooter,
|
||||
TldrawUiDialogHeader,
|
||||
TldrawUiDialogTitle,
|
||||
TldrawUiIcon,
|
||||
TldrawUiInput,
|
||||
useReactor,
|
||||
useValue,
|
||||
} from 'tldraw'
|
||||
import { PROVIDERS, makeRealSettings } from '../lib/settings'
|
||||
import { SYSTEM_PROMPT } from '@/prompt'
|
||||
TLUiDialogProps,
|
||||
TldrawUiButton,
|
||||
TldrawUiButtonLabel,
|
||||
TldrawUiDialogBody,
|
||||
TldrawUiDialogCloseButton,
|
||||
TldrawUiDialogFooter,
|
||||
TldrawUiDialogHeader,
|
||||
TldrawUiDialogTitle,
|
||||
TldrawUiInput,
|
||||
} from "tldraw"
|
||||
import React from "react"
|
||||
|
||||
export function SettingsDialog({ onClose }: TLUiDialogProps) {
|
||||
const settings = useValue('settings', () => makeRealSettings.get(), [])
|
||||
const [apiKey, setApiKey] = React.useState(() => {
|
||||
return localStorage.getItem("openai_api_key") || ""
|
||||
})
|
||||
|
||||
useReactor(
|
||||
'update settings local storage',
|
||||
() => {
|
||||
localStorage.setItem('makereal_settings_2', JSON.stringify(makeRealSettings.get()))
|
||||
},
|
||||
[]
|
||||
)
|
||||
const handleChange = (value: string) => {
|
||||
setApiKey(value)
|
||||
localStorage.setItem("openai_api_key", value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TldrawUiDialogHeader>
|
||||
<TldrawUiDialogTitle>Settings</TldrawUiDialogTitle>
|
||||
<TldrawUiDialogCloseButton />
|
||||
</TldrawUiDialogHeader>
|
||||
<TldrawUiDialogBody
|
||||
style={{ maxWidth: 350, display: 'flex', flexDirection: 'column', gap: 8 }}
|
||||
>
|
||||
<p>
|
||||
To use Make Real, enter your API key for each model provider that you wish to use. Draw
|
||||
some shapes, then select the shapes and click Make Real.{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://tldraw.notion.site/Make-Real-FAQs-93be8b5273d14f7386e14eb142575e6e?pvs=4"
|
||||
>
|
||||
<u>Read our guide.</u>
|
||||
</a>
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 8 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: 4 }}>
|
||||
<label style={{ flexGrow: 2 }}>Provider</label>
|
||||
</div>
|
||||
<select
|
||||
className="apikey_select"
|
||||
value={settings.provider}
|
||||
onChange={(e) => {
|
||||
makeRealSettings.update((s) => ({ ...s, provider: e.target.value as any }))
|
||||
}}
|
||||
>
|
||||
{PROVIDERS.map((provider) => {
|
||||
return (
|
||||
<option key={provider.id + 'option'} value={provider.id}>
|
||||
{provider.name}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
<option value="all">All</option>
|
||||
</select>
|
||||
{settings.provider !== 'all' && (
|
||||
<>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: 4 }}>
|
||||
<label style={{ flexGrow: 2 }}>Model</label>
|
||||
</div>
|
||||
<select
|
||||
className="apikey_select"
|
||||
value={settings.models[settings.provider]}
|
||||
onChange={(e) => {
|
||||
makeRealSettings.update((s) => ({
|
||||
...s,
|
||||
models: { ...s.models, [settings.provider]: e.target.value as any },
|
||||
}))
|
||||
}}
|
||||
>
|
||||
{PROVIDERS.find((p) => p.id === settings.provider)!.models.map((model) => {
|
||||
return (
|
||||
<option key={model + 'option'} value={model}>
|
||||
{model}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<hr style={{ margin: '12px 0px' }} />
|
||||
{PROVIDERS.map((provider) => {
|
||||
if (provider.id === 'google') return null
|
||||
const value = settings.keys[provider.id as keyof typeof settings.keys]
|
||||
return (
|
||||
<ApiKeyInput
|
||||
provider={provider}
|
||||
key={provider.name + 'key'}
|
||||
value={value}
|
||||
warning={
|
||||
value === '' && (settings.provider === provider.id || settings.provider === 'all')
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{/* <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: 4 }}>
|
||||
<label style={{ flexGrow: 2 }}>Google</label>
|
||||
</div>
|
||||
<TldrawUiInput
|
||||
className="apikey_input"
|
||||
value={settings.keys.google}
|
||||
placeholder="risky but cool"
|
||||
onValueChange={(value) => {
|
||||
const next = { ...settings, keys: { ...settings.keys, google: value } }
|
||||
localStorage.setItem('makereal_settings_2', JSON.stringify(next))
|
||||
makeRealSettings.set(next)
|
||||
}}
|
||||
/>
|
||||
</div> */}
|
||||
<hr style={{ margin: '12px 0px' }} />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: 4 }}>
|
||||
<label style={{ flexGrow: 2 }}>System prompt</label>
|
||||
<button
|
||||
style={{ all: 'unset', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
makeRealSettings.update((s) => ({
|
||||
...s,
|
||||
prompts: { ...s.prompts, system: SYSTEM_PROMPT },
|
||||
}))
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<TldrawUiInput
|
||||
className="apikey_input"
|
||||
value={settings.prompts.system}
|
||||
onValueChange={(value) => {
|
||||
makeRealSettings.update((s) => ({ ...s, prompts: { ...s.prompts, system: value } }))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TldrawUiDialogBody>
|
||||
<TldrawUiDialogFooter className="tlui-dialog__footer__actions">
|
||||
<TldrawUiButton
|
||||
type="primary"
|
||||
onClick={async () => {
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<TldrawUiButtonLabel>Save</TldrawUiButtonLabel>
|
||||
</TldrawUiButton>
|
||||
</TldrawUiDialogFooter>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<TldrawUiDialogHeader>
|
||||
<TldrawUiDialogTitle>API Keys</TldrawUiDialogTitle>
|
||||
<TldrawUiDialogCloseButton />
|
||||
</TldrawUiDialogHeader>
|
||||
<TldrawUiDialogBody style={{ maxWidth: 350 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<label>OpenAI API Key</label>
|
||||
<TldrawUiInput
|
||||
value={apiKey}
|
||||
placeholder="Enter your OpenAI API key"
|
||||
onValueChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</TldrawUiDialogBody>
|
||||
<TldrawUiDialogFooter>
|
||||
<TldrawUiButton type="primary" onClick={onClose}>
|
||||
<TldrawUiButtonLabel>Close</TldrawUiButtonLabel>
|
||||
</TldrawUiButton>
|
||||
</TldrawUiDialogFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ApiKeyInput({
|
||||
provider,
|
||||
value,
|
||||
warning,
|
||||
}: {
|
||||
provider: (typeof PROVIDERS)[number]
|
||||
value: string
|
||||
warning: boolean
|
||||
}) {
|
||||
const isValid = value.length === 0 || provider.validate(value)
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', gap: 4, alignItems: 'center' }}>
|
||||
<label style={{ flexGrow: 2, color: warning ? 'red' : 'var(--color-text)' }}>
|
||||
{provider.name} API key
|
||||
</label>
|
||||
<a style={{ cursor: 'pointer', pointerEvents: 'all' }} target="_blank" href={provider.help}>
|
||||
<TldrawUiIcon
|
||||
className="apikey_help_icon"
|
||||
small
|
||||
icon={provider.validate(value) ? 'check' : 'question-mark-circle'}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<TldrawUiInput
|
||||
className={`apikey_input ${isValid ? '' : 'apikey_input__invalid'}`}
|
||||
value={value}
|
||||
placeholder="risky but cool"
|
||||
onValueChange={(value) => {
|
||||
makeRealSettings.update((s) => ({ ...s, keys: { ...s.keys, [provider.id]: value } }))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +1,112 @@
|
|||
import { CustomMainMenu } from "./CustomMainMenu"
|
||||
import { CustomToolbar } from "./CustomToolbar"
|
||||
import { CustomContextMenu } from "./CustomContextMenu"
|
||||
import { TLComponents } from "tldraw"
|
||||
import {
|
||||
DefaultKeyboardShortcutsDialog,
|
||||
DefaultKeyboardShortcutsDialogContent,
|
||||
DefaultToolbar,
|
||||
DefaultToolbarContent,
|
||||
TLComponents,
|
||||
TldrawUiMenuItem,
|
||||
useDialogs,
|
||||
useIsToolSelected,
|
||||
useTools,
|
||||
} from "tldraw"
|
||||
import { SettingsDialog } from "./SettingsDialog"
|
||||
import { useEffect } from "react"
|
||||
import { SlidesPanel } from "@/slides/SlidesPanel"
|
||||
import { useState } from "react"
|
||||
|
||||
export const components: TLComponents = {
|
||||
Toolbar: CustomToolbar,
|
||||
// Toolbar: CustomToolbar,
|
||||
MainMenu: CustomMainMenu,
|
||||
ContextMenu: CustomContextMenu,
|
||||
HelperButtons: SlidesPanel,
|
||||
Toolbar: (props: any) => {
|
||||
const tools = useTools()
|
||||
const slideTool = tools["Slide"]
|
||||
const isSlideSelected = slideTool ? useIsToolSelected(slideTool) : false
|
||||
const { addDialog, removeDialog } = useDialogs()
|
||||
const [hasApiKey, setHasApiKey] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const key = localStorage.getItem("openai_api_key")
|
||||
setHasApiKey(!!key)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", width: "100%", height: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: "40px",
|
||||
right: "12px",
|
||||
zIndex: 99999,
|
||||
pointerEvents: "auto",
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
(
|
||||
<button
|
||||
onClick={() => {
|
||||
addDialog({
|
||||
id: "api-keys",
|
||||
component: ({ onClose }: { onClose: () => void }) => (
|
||||
<SettingsDialog
|
||||
onClose={() => {
|
||||
onClose()
|
||||
removeDialog("api-keys")
|
||||
const settings = localStorage.getItem("jeff_keys")
|
||||
if (settings) {
|
||||
const { keys } = JSON.parse(settings)
|
||||
setHasApiKey(Object.values(keys).some((key) => key))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
borderRadius: "4px",
|
||||
background: "#2F80ED",
|
||||
color: "white",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
fontWeight: 500,
|
||||
transition: "background 0.2s ease",
|
||||
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
||||
whiteSpace: "nowrap",
|
||||
userSelect: "none",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = "#1366D6"
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = "#2F80ED"
|
||||
}}
|
||||
>
|
||||
Keys {hasApiKey ? "✅" : "❌"}
|
||||
</button>
|
||||
)
|
||||
</div>
|
||||
<DefaultToolbar {...props}>
|
||||
{slideTool && (
|
||||
<TldrawUiMenuItem {...slideTool} isSelected={isSlideSelected} />
|
||||
)}
|
||||
<DefaultToolbarContent />
|
||||
</DefaultToolbar>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
KeyboardShortcutsDialog: (props: any) => {
|
||||
const tools = useTools()
|
||||
return (
|
||||
<DefaultKeyboardShortcutsDialog {...props}>
|
||||
<TldrawUiMenuItem {...tools["Slide"]} />
|
||||
<DefaultKeyboardShortcutsDialogContent />
|
||||
</DefaultKeyboardShortcutsDialog>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import {
|
|||
import { saveToPdf } from "../utils/pdfUtils"
|
||||
import { searchText } from "../utils/searchUtils"
|
||||
import { EmbedShape, IEmbedShape } from "@/shapes/EmbedShapeUtil"
|
||||
import { moveToSlide } from "@/slides/useSlides"
|
||||
import { ISlideShape } from "@/shapes/SlideShapeUtil"
|
||||
|
||||
export const overrides: TLUiOverrides = {
|
||||
tools(editor, tools) {
|
||||
|
|
@ -44,19 +46,19 @@ export const overrides: TLUiOverrides = {
|
|||
//TODO: Fix double click to zoom on selector tool later...
|
||||
onDoubleClick: (info: any) => {
|
||||
const shape = editor.getShapeAtPoint(info.point)
|
||||
if (shape?.type === 'Embed') {
|
||||
if (shape?.type === "Embed") {
|
||||
// Let the Embed shape handle its own double-click behavior
|
||||
const util = editor.getShapeUtil(shape) as EmbedShape
|
||||
util?.onDoubleClick?.(shape as IEmbedShape)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Handle all pointer types (mouse, touch, pen)
|
||||
const point = info.point || (info.touches && info.touches[0]) || info
|
||||
|
||||
|
||||
// Zoom in at the clicked/touched point
|
||||
editor.zoomIn(point, { animation: { duration: 200 } })
|
||||
|
||||
|
||||
// Stop event propagation and prevent default handling
|
||||
info.stopPropagation?.()
|
||||
return false
|
||||
|
|
@ -97,10 +99,10 @@ export const overrides: TLUiOverrides = {
|
|||
type: "Slide",
|
||||
readonlyOk: true,
|
||||
onSelect: () => {
|
||||
console.log('SlideShape tool selected from menu')
|
||||
console.log('Current tool before:', editor.getCurrentToolId())
|
||||
console.log("SlideShape tool selected from menu")
|
||||
console.log("Current tool before:", editor.getCurrentToolId())
|
||||
editor.setCurrentTool("Slide")
|
||||
console.log('Current tool after:', editor.getCurrentToolId())
|
||||
console.log("Current tool after:", editor.getCurrentToolId())
|
||||
},
|
||||
},
|
||||
Markdown: {
|
||||
|
|
@ -370,6 +372,66 @@ export const overrides: TLUiOverrides = {
|
|||
readonlyOk: true,
|
||||
onSelect: () => searchText(editor),
|
||||
},
|
||||
"next-slide": {
|
||||
id: "next-slide",
|
||||
label: "Next slide",
|
||||
kbd: "right",
|
||||
onSelect() {
|
||||
const slides = editor
|
||||
.getCurrentPageShapes()
|
||||
.filter((shape) => shape.type === "Slide")
|
||||
if (slides.length === 0) return
|
||||
|
||||
const currentSlide = editor
|
||||
.getSelectedShapes()
|
||||
.find((shape) => shape.type === "Slide")
|
||||
const currentIndex = currentSlide
|
||||
? slides.findIndex((slide) => slide.id === currentSlide.id)
|
||||
: -1
|
||||
|
||||
// Calculate next index with wraparound
|
||||
const nextIndex =
|
||||
currentIndex === -1
|
||||
? 0
|
||||
: currentIndex >= slides.length - 1
|
||||
? 0
|
||||
: currentIndex + 1
|
||||
|
||||
const nextSlide = slides[nextIndex]
|
||||
|
||||
editor.select(nextSlide.id)
|
||||
editor.stopCameraAnimation()
|
||||
moveToSlide(editor, nextSlide as ISlideShape)
|
||||
},
|
||||
},
|
||||
"previous-slide": {
|
||||
id: "previous-slide",
|
||||
label: "Previous slide",
|
||||
kbd: "left",
|
||||
onSelect() {
|
||||
const slides = editor
|
||||
.getCurrentPageShapes()
|
||||
.filter((shape) => shape.type === "Slide")
|
||||
if (slides.length === 0) return
|
||||
|
||||
const currentSlide = editor
|
||||
.getSelectedShapes()
|
||||
.find((shape) => shape.type === "Slide")
|
||||
const currentIndex = currentSlide
|
||||
? slides.findIndex((slide) => slide.id === currentSlide.id)
|
||||
: -1
|
||||
|
||||
// Calculate previous index with wraparound
|
||||
const previousIndex =
|
||||
currentIndex <= 0 ? slides.length - 1 : currentIndex - 1
|
||||
|
||||
const previousSlide = slides[previousIndex]
|
||||
|
||||
editor.select(previousSlide.id)
|
||||
editor.stopCameraAnimation()
|
||||
moveToSlide(editor, previousSlide as ISlideShape)
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import OpenAI from "openai";
|
||||
|
||||
const apiKey = localStorage.getItem("openai_api_key") || ""
|
||||
const openai = new OpenAI({
|
||||
apiKey,
|
||||
dangerouslyAllowBrowser: true,
|
||||
});
|
||||
|
||||
export async function llm(
|
||||
systemPrompt: string,
|
||||
userPrompt: string,
|
||||
onToken: (partialResponse: string, done: boolean) => void,
|
||||
) {
|
||||
if (!apiKey) {
|
||||
throw new Error("No API key found")
|
||||
}
|
||||
console.log("System Prompt:", systemPrompt);
|
||||
console.log("User Prompt:", userPrompt);
|
||||
let partial = "";
|
||||
const stream = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{ role: "system", content: 'You are a helpful assistant.' },
|
||||
{ role: "user", content: userPrompt },
|
||||
],
|
||||
stream: true,
|
||||
});
|
||||
for await (const chunk of stream) {
|
||||
partial += chunk.choices[0]?.delta?.content || "";
|
||||
onToken(partial, false);
|
||||
}
|
||||
console.log("Generated:", partial);
|
||||
onToken(partial, true);
|
||||
}
|
||||
Loading…
Reference in New Issue