fix 'c', mycofi room,remove gesture tool
This commit is contained in:
parent
8fa8c388d9
commit
1b0fc57779
|
|
@ -0,0 +1,63 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const inputFile = '/home/jeffe/Github/canvas-website/src/shapes/mycofi_room.json';
|
||||||
|
const outputFile = '/home/jeffe/Github/canvas-website/src/shapes/mycofi_room_fixed.json';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the JSON file
|
||||||
|
const data = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
|
||||||
|
|
||||||
|
let fixedCount = 0;
|
||||||
|
let removedCount = 0;
|
||||||
|
|
||||||
|
// Process all documents
|
||||||
|
data.documents = data.documents.filter(doc => {
|
||||||
|
if (doc.state && doc.state.typeName === 'shape' && doc.state.type === 'draw') {
|
||||||
|
const segments = doc.state.props?.segments;
|
||||||
|
if (segments) {
|
||||||
|
// Check each segment for single-point issues
|
||||||
|
const validSegments = segments.filter(segment => {
|
||||||
|
if (segment.points && segment.points.length === 1) {
|
||||||
|
// For single-point segments, we have two options:
|
||||||
|
// 1. Remove the segment entirely
|
||||||
|
// 2. Add a second point to make it valid
|
||||||
|
|
||||||
|
// Let's remove single-point segments as they're likely incomplete
|
||||||
|
removedCount++;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validSegments.length === 0) {
|
||||||
|
// If no valid segments remain, remove the entire shape
|
||||||
|
removedCount++;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// Update the segments
|
||||||
|
doc.state.props.segments = validSegments;
|
||||||
|
fixedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write the fixed data
|
||||||
|
fs.writeFileSync(outputFile, JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
console.log(`Successfully fixed draw shapes:`);
|
||||||
|
console.log(`- Fixed shapes: ${fixedCount}`);
|
||||||
|
console.log(`- Removed invalid shapes: ${removedCount}`);
|
||||||
|
console.log(`- Output saved to: ${outputFile}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fixing draw shapes:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const inputFile = '/home/jeffe/Github/canvas-website/src/shapes/mycofi_room.json';
|
||||||
|
const outputFile = '/home/jeffe/Github/canvas-website/src/shapes/mycofi_room_fixed.json';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read the JSON file
|
||||||
|
const data = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
|
||||||
|
|
||||||
|
let fixedCount = 0;
|
||||||
|
let removedCount = 0;
|
||||||
|
|
||||||
|
// Process all documents
|
||||||
|
data.documents = data.documents.filter(doc => {
|
||||||
|
if (doc.state && doc.state.typeName === 'shape') {
|
||||||
|
const state = doc.state;
|
||||||
|
const x = state.x || 0;
|
||||||
|
const y = state.y || 0;
|
||||||
|
|
||||||
|
// Check for extremely large coordinates that could cause hit testing issues
|
||||||
|
if (Math.abs(x) > 100000 || Math.abs(y) > 100000 ||
|
||||||
|
!isFinite(x) || !isFinite(y)) {
|
||||||
|
|
||||||
|
console.log(`Fixing shape ${state.id} with extreme position: (${x}, ${y})`);
|
||||||
|
|
||||||
|
// Reset to a reasonable position (center of canvas)
|
||||||
|
state.x = 0;
|
||||||
|
state.y = 0;
|
||||||
|
fixedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for extremely large dimensions
|
||||||
|
if (state.props) {
|
||||||
|
const w = state.props.w || 0;
|
||||||
|
const h = state.props.h || 0;
|
||||||
|
|
||||||
|
if (w > 100000 || h > 100000 || !isFinite(w) || !isFinite(h)) {
|
||||||
|
console.log(`Fixing shape ${state.id} with extreme dimensions: ${w}x${h}`);
|
||||||
|
|
||||||
|
// Reset to reasonable default dimensions
|
||||||
|
state.props.w = Math.min(w, 200);
|
||||||
|
state.props.h = Math.min(h, 200);
|
||||||
|
fixedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for invalid rotation values
|
||||||
|
if (state.rotation !== undefined && !isFinite(state.rotation)) {
|
||||||
|
console.log(`Fixing shape ${state.id} with invalid rotation: ${state.rotation}`);
|
||||||
|
state.rotation = 0;
|
||||||
|
fixedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for draw shapes with problematic segments
|
||||||
|
if (state.type === 'draw' && state.props?.segments) {
|
||||||
|
const validSegments = state.props.segments.filter(segment => {
|
||||||
|
if (segment.points && segment.points.length === 1) {
|
||||||
|
// Remove single-point segments as they can cause hit testing issues
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validSegments.length === 0) {
|
||||||
|
// If no valid segments remain, remove the entire shape
|
||||||
|
console.log(`Removing shape ${state.id} with no valid segments`);
|
||||||
|
removedCount++;
|
||||||
|
return false;
|
||||||
|
} else if (validSegments.length !== state.props.segments.length) {
|
||||||
|
// Update the segments
|
||||||
|
state.props.segments = validSegments;
|
||||||
|
fixedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write the fixed data
|
||||||
|
fs.writeFileSync(outputFile, JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
|
console.log(`\\nSuccessfully fixed board data:`);
|
||||||
|
console.log(`- Fixed shapes: ${fixedCount}`);
|
||||||
|
console.log(`- Removed invalid shapes: ${removedCount}`);
|
||||||
|
console.log(`- Output saved to: ${outputFile}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fixing board data:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -10,7 +10,6 @@ import {
|
||||||
id: 'toggle-graph-layout',
|
id: 'toggle-graph-layout',
|
||||||
label: 'Toggle Graph Layout' as TLUiTranslationKey,
|
label: 'Toggle Graph Layout' as TLUiTranslationKey,
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
kbd: 'g',
|
|
||||||
onSelect(_source: TLUiEventSource) {
|
onSelect(_source: TLUiEventSource) {
|
||||||
const event = new CustomEvent('toggleGraphLayoutEvent');
|
const event = new CustomEvent('toggleGraphLayoutEvent');
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,20 @@ const unpackShape = (shape: any) => {
|
||||||
return prop !== undefined ? constructor(prop) : undefined;
|
return prop !== undefined ? constructor(prop) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only add text property for shapes that support it (like text shapes)
|
// Handle text shapes properly - convert text property to richText if needed
|
||||||
const shapeProps = { ...props }
|
const shapeProps = { ...props }
|
||||||
if (type === 'text' && props.text !== undefined) {
|
if (type === 'text') {
|
||||||
shapeProps.text = cast(props.text, String)
|
// Remove any text property as it's not valid for TLDraw text shapes
|
||||||
|
if ('text' in shapeProps) {
|
||||||
|
delete shapeProps.text
|
||||||
|
}
|
||||||
|
// Ensure richText exists for text shapes
|
||||||
|
if (!shapeProps.richText) {
|
||||||
|
shapeProps.richText = {
|
||||||
|
content: [],
|
||||||
|
type: 'doc'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ import {
|
||||||
} from "@/ui/cameraUtils"
|
} from "@/ui/cameraUtils"
|
||||||
import { Collection, initializeGlobalCollections } from "@/collections"
|
import { Collection, initializeGlobalCollections } from "@/collections"
|
||||||
import { GraphLayoutCollection } from "@/graph/GraphLayoutCollection"
|
import { GraphLayoutCollection } from "@/graph/GraphLayoutCollection"
|
||||||
import { GestureTool } from "@/GestureTool"
|
|
||||||
import { CmdK } from "@/CmdK"
|
import { CmdK } from "@/CmdK"
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -76,7 +75,6 @@ const customTools = [
|
||||||
MarkdownTool,
|
MarkdownTool,
|
||||||
PromptShapeTool,
|
PromptShapeTool,
|
||||||
SharedPianoTool,
|
SharedPianoTool,
|
||||||
GestureTool,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export function Board() {
|
export function Board() {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
65
src/ui.tsx
65
src/ui.tsx
|
|
@ -9,81 +9,16 @@ import {
|
||||||
TLDrawShape,
|
TLDrawShape,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
} from "tldraw"
|
} from "tldraw"
|
||||||
import { DollarRecognizer } from "@/gestures"
|
|
||||||
import { DEFAULT_GESTURES } from "@/default_gestures"
|
|
||||||
|
|
||||||
export const overrides: TLUiOverrides = {
|
export const overrides: TLUiOverrides = {
|
||||||
tools(editor, tools) {
|
tools(editor, tools) {
|
||||||
return {
|
return {
|
||||||
...tools,
|
...tools,
|
||||||
gesture: {
|
|
||||||
id: "gesture",
|
|
||||||
name: "Gesture",
|
|
||||||
icon: "👆",
|
|
||||||
kbd: "g",
|
|
||||||
label: "Gesture",
|
|
||||||
onSelect: () => {
|
|
||||||
editor.setCurrentTool("gesture")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions(editor, actions): TLUiActionsContextType {
|
actions(editor, actions): TLUiActionsContextType {
|
||||||
const R = new DollarRecognizer(DEFAULT_GESTURES)
|
|
||||||
return {
|
return {
|
||||||
...actions,
|
...actions,
|
||||||
recognize: {
|
|
||||||
id: "recognize",
|
|
||||||
kbd: "c",
|
|
||||||
onSelect: () => {
|
|
||||||
const onlySelectedShape = editor.getOnlySelectedShape()
|
|
||||||
if (!onlySelectedShape || onlySelectedShape.type !== "draw") return
|
|
||||||
console.log("recognizing")
|
|
||||||
const verts = editor.getShapeGeometry(onlySelectedShape).vertices
|
|
||||||
const result = R.recognize(verts)
|
|
||||||
console.log(result)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
addGesture: {
|
|
||||||
id: "addGesture",
|
|
||||||
kbd: "x",
|
|
||||||
onSelect: () => {
|
|
||||||
const onlySelectedShape = editor.getOnlySelectedShape()
|
|
||||||
if (!onlySelectedShape || onlySelectedShape.type !== "draw") return
|
|
||||||
const name = onlySelectedShape.meta.name
|
|
||||||
if (!name) return
|
|
||||||
console.log("adding gesture:", name)
|
|
||||||
const points = editor.getShapeGeometry(onlySelectedShape).vertices
|
|
||||||
R.addGesture(name as string, points)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
recognizeAndSnap: {
|
|
||||||
id: "recognizeAndSnap",
|
|
||||||
kbd: "z",
|
|
||||||
onSelect: () => {
|
|
||||||
const onlySelectedShape = editor.getOnlySelectedShape()
|
|
||||||
if (!onlySelectedShape || onlySelectedShape.type !== "draw") return
|
|
||||||
const points = editor.getShapeGeometry(onlySelectedShape).vertices
|
|
||||||
const result = R.recognize(points)
|
|
||||||
console.log("morphing to closest:", result.name)
|
|
||||||
const newShape: TLShapePartial<TLDrawShape> = {
|
|
||||||
...onlySelectedShape,
|
|
||||||
type: "draw",
|
|
||||||
props: {
|
|
||||||
...onlySelectedShape.props,
|
|
||||||
segments: [
|
|
||||||
{
|
|
||||||
points: R.unistrokes
|
|
||||||
.find((u) => u.name === result.name)
|
|
||||||
?.originalPoints() || [],
|
|
||||||
type: "free",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
editor.animateShape(newShape)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,14 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
// Keyboard shortcut for adding to collection
|
// Keyboard shortcut for adding to collection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'c' && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
// Only trigger if not typing in a text input, textarea, or contenteditable element
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
const isTextInput = target.tagName === 'INPUT' ||
|
||||||
|
target.tagName === 'TEXTAREA' ||
|
||||||
|
target.contentEditable === 'true' ||
|
||||||
|
target.closest('[contenteditable="true"]')
|
||||||
|
|
||||||
|
if (event.key === 'c' && !event.ctrlKey && !event.altKey && !event.metaKey && !isTextInput) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (hasSelection && collection && !allSelectedShapesInCollection) {
|
if (hasSelection && collection && !allSelectedShapesInCollection) {
|
||||||
handleAddToCollection()
|
handleAddToCollection()
|
||||||
|
|
@ -173,7 +180,7 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
id="add-to-collection"
|
id="add-to-collection"
|
||||||
label="Add to Collection"
|
label="Add to Collection"
|
||||||
icon="plus"
|
icon="plus"
|
||||||
kbd="c"
|
kbd="alt+a"
|
||||||
disabled={!hasSelection || !collection}
|
disabled={!hasSelection || !collection}
|
||||||
onSelect={handleAddToCollection}
|
onSelect={handleAddToCollection}
|
||||||
/>
|
/>
|
||||||
|
|
@ -232,7 +239,6 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
id="search-text"
|
id="search-text"
|
||||||
label="Search Text"
|
label="Search Text"
|
||||||
icon="search"
|
icon="search"
|
||||||
kbd="s"
|
|
||||||
onSelect={() => searchText(editor)}
|
onSelect={() => searchText(editor)}
|
||||||
/>
|
/>
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Editor, useDefaultHelpers } from "tldraw"
|
import { Editor, useDefaultHelpers, createShapeId } from "tldraw"
|
||||||
import {
|
import {
|
||||||
shapeIdValidator,
|
shapeIdValidator,
|
||||||
TLArrowShape,
|
TLArrowShape,
|
||||||
|
|
@ -20,8 +20,9 @@ import { moveToSlide } from "@/slides/useSlides"
|
||||||
import { ISlideShape } from "@/shapes/SlideShapeUtil"
|
import { ISlideShape } from "@/shapes/SlideShapeUtil"
|
||||||
import { getEdge } from "@/propagators/tlgraph"
|
import { getEdge } from "@/propagators/tlgraph"
|
||||||
import { llm, getApiKey } from "@/utils/llmUtils"
|
import { llm, getApiKey } from "@/utils/llmUtils"
|
||||||
|
import type FileSystem from "@oddjs/odd/fs/index"
|
||||||
|
|
||||||
export const overrides: TLUiOverrides = {
|
export const createOverrides = (fileSystem?: FileSystem | null): TLUiOverrides => ({
|
||||||
tools(editor, tools) {
|
tools(editor, tools) {
|
||||||
return {
|
return {
|
||||||
...tools,
|
...tools,
|
||||||
|
|
@ -160,15 +161,6 @@ export const overrides: TLUiOverrides = {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => editor.setCurrentTool("SharedPiano"),
|
onSelect: () => editor.setCurrentTool("SharedPiano"),
|
||||||
},
|
},
|
||||||
gesture: {
|
|
||||||
id: "gesture",
|
|
||||||
icon: "draw",
|
|
||||||
label: "Gesture",
|
|
||||||
kbd: "g",
|
|
||||||
readonlyOk: true,
|
|
||||||
type: "gesture",
|
|
||||||
onSelect: () => editor.setCurrentTool("gesture"),
|
|
||||||
},
|
|
||||||
hand: {
|
hand: {
|
||||||
...tools.hand,
|
...tools.hand,
|
||||||
onDoubleClick: (info: any) => {
|
onDoubleClick: (info: any) => {
|
||||||
|
|
@ -190,7 +182,7 @@ export const overrides: TLUiOverrides = {
|
||||||
zoomToSelection: {
|
zoomToSelection: {
|
||||||
id: "zoom-to-selection",
|
id: "zoom-to-selection",
|
||||||
label: "Zoom to Selection",
|
label: "Zoom to Selection",
|
||||||
kbd: "z",
|
kbd: "alt+z",
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
if (editor.getSelectedShapeIds().length > 0) {
|
if (editor.getSelectedShapeIds().length > 0) {
|
||||||
zoomToSelection(editor)
|
zoomToSelection(editor)
|
||||||
|
|
@ -251,7 +243,6 @@ export const overrides: TLUiOverrides = {
|
||||||
moveSelectedLeft: {
|
moveSelectedLeft: {
|
||||||
id: "move-selected-left",
|
id: "move-selected-left",
|
||||||
label: "Move Left",
|
label: "Move Left",
|
||||||
kbd: "ArrowLeft",
|
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
const selectedShapes = editor.getSelectedShapes()
|
const selectedShapes = editor.getSelectedShapes()
|
||||||
if (selectedShapes.length > 0) {
|
if (selectedShapes.length > 0) {
|
||||||
|
|
@ -269,7 +260,6 @@ export const overrides: TLUiOverrides = {
|
||||||
moveSelectedRight: {
|
moveSelectedRight: {
|
||||||
id: "move-selected-right",
|
id: "move-selected-right",
|
||||||
label: "Move Right",
|
label: "Move Right",
|
||||||
kbd: "ArrowRight",
|
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
const selectedShapes = editor.getSelectedShapes()
|
const selectedShapes = editor.getSelectedShapes()
|
||||||
if (selectedShapes.length > 0) {
|
if (selectedShapes.length > 0) {
|
||||||
|
|
@ -287,7 +277,6 @@ export const overrides: TLUiOverrides = {
|
||||||
moveSelectedUp: {
|
moveSelectedUp: {
|
||||||
id: "move-selected-up",
|
id: "move-selected-up",
|
||||||
label: "Move Up",
|
label: "Move Up",
|
||||||
kbd: "ArrowUp",
|
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
const selectedShapes = editor.getSelectedShapes()
|
const selectedShapes = editor.getSelectedShapes()
|
||||||
if (selectedShapes.length > 0) {
|
if (selectedShapes.length > 0) {
|
||||||
|
|
@ -305,7 +294,6 @@ export const overrides: TLUiOverrides = {
|
||||||
moveSelectedDown: {
|
moveSelectedDown: {
|
||||||
id: "move-selected-down",
|
id: "move-selected-down",
|
||||||
label: "Move Down",
|
label: "Move Down",
|
||||||
kbd: "ArrowDown",
|
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
const selectedShapes = editor.getSelectedShapes()
|
const selectedShapes = editor.getSelectedShapes()
|
||||||
if (selectedShapes.length > 0) {
|
if (selectedShapes.length > 0) {
|
||||||
|
|
@ -323,7 +311,7 @@ export const overrides: TLUiOverrides = {
|
||||||
searchShapes: {
|
searchShapes: {
|
||||||
id: "search-shapes",
|
id: "search-shapes",
|
||||||
label: "Search Shapes",
|
label: "Search Shapes",
|
||||||
kbd: "s",
|
kbd: "alt+s",
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => searchText(editor),
|
onSelect: () => searchText(editor),
|
||||||
},
|
},
|
||||||
|
|
@ -333,58 +321,104 @@ export const overrides: TLUiOverrides = {
|
||||||
kbd: "alt+g",
|
kbd: "alt+g",
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
|
console.log("🎯 LLM action triggered")
|
||||||
|
|
||||||
const selectedShapes = editor.getSelectedShapes()
|
const selectedShapes = editor.getSelectedShapes()
|
||||||
|
console.log("Selected shapes:", selectedShapes.length, selectedShapes.map(s => s.type))
|
||||||
|
|
||||||
|
|
||||||
if (selectedShapes.length > 0) {
|
if (selectedShapes.length > 0) {
|
||||||
const selectedShape = selectedShapes[0] as TLArrowShape
|
const selectedShape = selectedShapes[0] as TLArrowShape
|
||||||
|
console.log("First selected shape type:", selectedShape.type)
|
||||||
|
|
||||||
|
|
||||||
if (selectedShape.type !== "arrow") {
|
if (selectedShape.type !== "arrow") {
|
||||||
|
console.log("❌ Selected shape is not an arrow, returning")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const edge = getEdge(selectedShape, editor)
|
const edge = getEdge(selectedShape, editor)
|
||||||
|
console.log("Edge found:", edge)
|
||||||
|
|
||||||
|
|
||||||
if (!edge) {
|
if (!edge) {
|
||||||
|
console.log("❌ No edge found, returning")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceShape = editor.getShape(edge.from)
|
const sourceShape = editor.getShape(edge.from)
|
||||||
|
const targetShape = editor.getShape(edge.to)
|
||||||
|
console.log("Arrow direction: FROM", sourceShape?.type, "TO", targetShape?.type)
|
||||||
|
|
||||||
const sourceText =
|
const sourceText =
|
||||||
sourceShape && sourceShape.type === "geo"
|
sourceShape && sourceShape.type === "geo"
|
||||||
? (sourceShape.meta as any)?.text || ""
|
? (sourceShape.meta as any)?.text || ""
|
||||||
: ""
|
: ""
|
||||||
|
console.log("Source shape:", sourceShape?.type, "Source text:", sourceText)
|
||||||
|
console.log("Target shape:", targetShape?.type, "Will generate content here")
|
||||||
|
|
||||||
|
|
||||||
const prompt = `Instruction: ${edge.text}
|
const prompt = `Instruction: ${edge.text}
|
||||||
${sourceText ? `Context: ${sourceText}` : ""}`;
|
${sourceText ? `Context: ${sourceText}` : ""}`;
|
||||||
|
console.log("Generated prompt:", prompt)
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log("🚀 Calling LLM with prompt...")
|
||||||
llm(prompt, (partialResponse: string) => {
|
llm(prompt, (partialResponse: string) => {
|
||||||
|
console.log("📝 LLM callback received:", partialResponse.substring(0, 100) + "...")
|
||||||
|
const targetShape = editor.getShape(edge.to)
|
||||||
|
console.log("Target shape for content generation:", targetShape?.type, "ID:", edge.to)
|
||||||
|
if (!targetShape) {
|
||||||
|
console.log("❌ No target shape found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const targetShape = editor.getShape(edge.to) as TLGeoShape
|
// Check if the target shape is a geo shape
|
||||||
editor.updateShape({
|
if (targetShape.type === "geo") {
|
||||||
id: edge.to,
|
console.log("✅ Updating existing geo shape with LLM response")
|
||||||
type: "geo",
|
editor.updateShape({
|
||||||
props: {
|
id: edge.to,
|
||||||
...targetShape.props,
|
type: "geo",
|
||||||
},
|
meta: {
|
||||||
meta: {
|
...targetShape.meta,
|
||||||
...targetShape.meta,
|
text: partialResponse,
|
||||||
text: partialResponse, // Store text in meta instead of props
|
},
|
||||||
},
|
})
|
||||||
})
|
console.log("✅ Content updated in target geo shape")
|
||||||
|
} else {
|
||||||
|
console.log("🆕 Target is not a geo shape, creating new geo shape at target location")
|
||||||
|
// If it's not a geo shape, create a new geo shape at the target location
|
||||||
|
const bounds = editor.getShapePageBounds(edge.to)
|
||||||
|
console.log("Target bounds:", bounds)
|
||||||
|
if (bounds) {
|
||||||
|
console.log("✅ Creating new geo shape with LLM response at target location")
|
||||||
|
editor.createShape({
|
||||||
|
id: createShapeId(),
|
||||||
|
type: "geo",
|
||||||
|
x: bounds.x,
|
||||||
|
y: bounds.y,
|
||||||
|
props: {
|
||||||
|
w: Math.max(200, partialResponse.length * 8),
|
||||||
|
h: 100,
|
||||||
|
geo: "rectangle",
|
||||||
|
color: "black",
|
||||||
|
fill: "none",
|
||||||
|
dash: "draw",
|
||||||
|
size: "m",
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
text: partialResponse,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.log("✅ New geo shape created with LLM response")
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error calling LLM:", error);
|
console.error("Error calling LLM:", error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
console.log("❌ No shapes selected")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,221 +1,212 @@
|
||||||
import OpenAI from "openai";
|
import OpenAI from "openai";
|
||||||
import Anthropic from "@anthropic-ai/sdk";
|
import Anthropic from "@anthropic-ai/sdk";
|
||||||
import { makeRealSettings } from "@/lib/settings";
|
import type FileSystem from "@oddjs/odd/fs/index";
|
||||||
|
|
||||||
|
// Helper function to validate API keys
|
||||||
|
function isValidApiKey(provider: string, key: string): boolean {
|
||||||
|
if (!key || typeof key !== 'string' || key.trim() === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (provider) {
|
||||||
|
case 'openai':
|
||||||
|
return key.startsWith('sk-') && key.length > 20;
|
||||||
|
case 'anthropic':
|
||||||
|
return key.startsWith('sk-ant-') && key.length > 20;
|
||||||
|
case 'google':
|
||||||
|
return key.length > 20; // Google keys don't have a specific prefix
|
||||||
|
default:
|
||||||
|
return key.length > 10; // Generic validation for unknown providers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to load API keys from user profile filesystem
|
||||||
|
async function loadApiKeysFromProfile(fs: FileSystem | null): Promise<{ openai: string; anthropic: string }> {
|
||||||
|
const result = { openai: '', anthropic: '' };
|
||||||
|
|
||||||
|
if (!fs) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to read API keys from user profile settings
|
||||||
|
const settingsPath = ['private', 'settings', 'api_keys.json'];
|
||||||
|
const filePath = (window as any).webnative?.path?.file(...settingsPath);
|
||||||
|
|
||||||
|
if (filePath && await fs.exists(filePath)) {
|
||||||
|
const fileContent = await fs.read(filePath);
|
||||||
|
const settings = JSON.parse(new TextDecoder().decode(fileContent));
|
||||||
|
|
||||||
|
if (settings.openai && typeof settings.openai === 'string') {
|
||||||
|
result.openai = settings.openai;
|
||||||
|
}
|
||||||
|
if (settings.anthropic && typeof settings.anthropic === 'string') {
|
||||||
|
result.anthropic = settings.anthropic;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📁 Loaded API keys from user profile");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("⚠️ Could not load API keys from user profile:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to save API keys to user profile filesystem
|
||||||
|
async function saveApiKeysToProfile(fs: FileSystem | null, openaiKey: string, anthropicKey: string): Promise<void> {
|
||||||
|
if (!fs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settings = {
|
||||||
|
openai: openaiKey,
|
||||||
|
anthropic: anthropicKey,
|
||||||
|
updated: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingsPath = ['private', 'settings', 'api_keys.json'];
|
||||||
|
const filePath = (window as any).webnative?.path?.file(...settingsPath);
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
const content = new TextEncoder().encode(JSON.stringify(settings, null, 2));
|
||||||
|
await fs.write(filePath, content);
|
||||||
|
await fs.publish();
|
||||||
|
console.log("💾 Saved API keys to user profile");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("⚠️ Could not save API keys to user profile:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function llm(
|
export async function llm(
|
||||||
userPrompt: string,
|
userPrompt: string,
|
||||||
onToken: (partialResponse: string, done?: boolean) => void,
|
onToken: (partialResponse: string, done?: boolean) => void,
|
||||||
|
fileSystem?: FileSystem | null,
|
||||||
) {
|
) {
|
||||||
// Validate the callback function
|
// Validate the callback function
|
||||||
if (typeof onToken !== 'function') {
|
if (typeof onToken !== 'function') {
|
||||||
throw new Error("onToken must be a function");
|
throw new Error("onToken must be a function");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-migrate old format API keys if needed
|
// Load API keys from both localStorage and user profile
|
||||||
await autoMigrateAPIKeys();
|
const localOpenaiKey = localStorage.getItem("openai_api_key") || "";
|
||||||
|
const localAnthropicKey = localStorage.getItem("anthropic_api_key") || "";
|
||||||
|
|
||||||
// Get current settings and available API keys
|
// Load from user profile if filesystem is available
|
||||||
let settings;
|
const profileKeys = await loadApiKeysFromProfile(fileSystem || null);
|
||||||
try {
|
|
||||||
settings = makeRealSettings.get()
|
// Use profile keys if available, otherwise fall back to localStorage
|
||||||
} catch (e) {
|
const openaiKey = profileKeys.openai || localOpenaiKey;
|
||||||
settings = null;
|
const anthropicKey = profileKeys.anthropic || localAnthropicKey;
|
||||||
|
|
||||||
|
console.log("🔑 OpenAI key present:", !!openaiKey && isValidApiKey('openai', openaiKey));
|
||||||
|
console.log("🔑 Anthropic key present:", !!anthropicKey && isValidApiKey('anthropic', anthropicKey));
|
||||||
|
console.log("📁 Profile keys loaded:", { openai: !!profileKeys.openai, anthropic: !!profileKeys.anthropic });
|
||||||
|
console.log("💾 Local keys loaded:", { openai: !!localOpenaiKey, anthropic: !!localAnthropicKey });
|
||||||
|
|
||||||
|
// Determine which provider to use
|
||||||
|
let provider: string | null = null;
|
||||||
|
let apiKey: string | null = null;
|
||||||
|
|
||||||
|
// Try OpenAI first if available
|
||||||
|
if (openaiKey && isValidApiKey('openai', openaiKey)) {
|
||||||
|
provider = 'openai';
|
||||||
|
apiKey = openaiKey;
|
||||||
}
|
}
|
||||||
|
// Try Anthropic if OpenAI not available
|
||||||
// Fallback to direct localStorage if makeRealSettings fails
|
else if (anthropicKey && isValidApiKey('anthropic', anthropicKey)) {
|
||||||
if (!settings) {
|
provider = 'anthropic';
|
||||||
try {
|
apiKey = anthropicKey;
|
||||||
const rawSettings = localStorage.getItem("openai_api_key");
|
|
||||||
if (rawSettings) {
|
|
||||||
settings = JSON.parse(rawSettings);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Continue with default settings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default settings if everything fails
|
|
||||||
if (!settings) {
|
|
||||||
settings = {
|
|
||||||
provider: 'openai',
|
|
||||||
models: { openai: 'gpt-4o', anthropic: 'claude-3-5-sonnet-20241022' },
|
|
||||||
keys: { openai: '', anthropic: '', google: '' }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableKeys = settings.keys || {}
|
|
||||||
|
|
||||||
// Determine which provider to use based on available keys
|
|
||||||
let provider: string | null = null
|
|
||||||
let apiKey: string | null = null
|
|
||||||
|
|
||||||
// Check if we have a preferred provider with a valid key
|
|
||||||
if (settings.provider && availableKeys[settings.provider as keyof typeof availableKeys] && availableKeys[settings.provider as keyof typeof availableKeys].trim() !== '') {
|
|
||||||
provider = settings.provider
|
|
||||||
apiKey = availableKeys[settings.provider as keyof typeof availableKeys]
|
|
||||||
} else {
|
|
||||||
// Fallback: use the first available provider with a valid key
|
|
||||||
for (const [key, value] of Object.entries(availableKeys)) {
|
|
||||||
if (typeof value === 'string' && value.trim() !== '') {
|
|
||||||
provider = key
|
|
||||||
apiKey = value
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!provider || !apiKey) {
|
if (!provider || !apiKey) {
|
||||||
// Try to get keys directly from localStorage as fallback
|
throw new Error("No valid API key found. Please set either 'openai_api_key' or 'anthropic_api_key' in localStorage.");
|
||||||
try {
|
|
||||||
const directSettings = localStorage.getItem("openai_api_key");
|
|
||||||
if (directSettings) {
|
|
||||||
// Check if it's the old format (just a string)
|
|
||||||
if (directSettings.startsWith('sk-') && !directSettings.startsWith('{')) {
|
|
||||||
// This is an old format OpenAI key, use it
|
|
||||||
provider = 'openai';
|
|
||||||
apiKey = directSettings;
|
|
||||||
} else {
|
|
||||||
// Try to parse as JSON
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(directSettings);
|
|
||||||
if (parsed.keys) {
|
|
||||||
for (const [key, value] of Object.entries(parsed.keys)) {
|
|
||||||
if (typeof value === 'string' && value.trim() !== '') {
|
|
||||||
provider = key;
|
|
||||||
apiKey = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
// If it's not JSON and starts with sk-, treat as old format OpenAI key
|
|
||||||
if (directSettings.startsWith('sk-')) {
|
|
||||||
provider = 'openai';
|
|
||||||
apiKey = directSettings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Continue with error handling
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!provider || !apiKey) {
|
|
||||||
throw new Error("No valid API key found for any provider")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = settings.models[provider] || getDefaultModel(provider)
|
console.log(`✅ Using ${provider} API`);
|
||||||
let partial = "";
|
|
||||||
|
|
||||||
|
// Try the selected provider
|
||||||
try {
|
try {
|
||||||
if (provider === 'openai') {
|
await tryProvider(provider, apiKey, getDefaultModel(provider), userPrompt, onToken);
|
||||||
const openai = new OpenAI({
|
console.log(`✅ Successfully used ${provider} API`);
|
||||||
apiKey,
|
} catch (error: any) {
|
||||||
dangerouslyAllowBrowser: true,
|
console.error(`❌ ${provider} API failed:`, error.message);
|
||||||
});
|
|
||||||
|
// If the first provider failed, try the other one
|
||||||
const stream = await openai.chat.completions.create({
|
const otherProvider = provider === 'openai' ? 'anthropic' : 'openai';
|
||||||
model: model,
|
const otherKey = otherProvider === 'openai' ? openaiKey : anthropicKey;
|
||||||
messages: [
|
|
||||||
{ role: "system", content: 'You are a helpful assistant.' },
|
if (otherKey && isValidApiKey(otherProvider, otherKey)) {
|
||||||
{ role: "user", content: userPrompt },
|
console.log(`🔄 Trying fallback ${otherProvider} API`);
|
||||||
],
|
try {
|
||||||
stream: true,
|
await tryProvider(otherProvider, otherKey, getDefaultModel(otherProvider), userPrompt, onToken);
|
||||||
});
|
console.log(`✅ Successfully used fallback ${otherProvider} API`);
|
||||||
|
return;
|
||||||
for await (const chunk of stream) {
|
} catch (fallbackError: any) {
|
||||||
const content = chunk.choices[0]?.delta?.content || "";
|
console.error(`❌ Fallback ${otherProvider} API also failed:`, fallbackError.message);
|
||||||
partial += content;
|
|
||||||
onToken(partial, false);
|
|
||||||
}
|
}
|
||||||
} else if (provider === 'anthropic') {
|
|
||||||
const anthropic = new Anthropic({
|
|
||||||
apiKey,
|
|
||||||
dangerouslyAllowBrowser: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const stream = await anthropic.messages.create({
|
|
||||||
model: model,
|
|
||||||
max_tokens: 4096,
|
|
||||||
messages: [
|
|
||||||
{ role: "user", content: userPrompt }
|
|
||||||
],
|
|
||||||
stream: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
for await (const chunk of stream) {
|
|
||||||
if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
|
|
||||||
const content = chunk.delta.text || "";
|
|
||||||
partial += content;
|
|
||||||
onToken(partial, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unsupported provider: ${provider}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onToken(partial, true);
|
// If all providers failed, throw the original error
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-migration function that runs automatically
|
async function tryProvider(provider: string, apiKey: string, model: string, userPrompt: string, onToken: (partialResponse: string, done?: boolean) => void) {
|
||||||
async function autoMigrateAPIKeys() {
|
let partial = "";
|
||||||
try {
|
|
||||||
const raw = localStorage.getItem("openai_api_key");
|
if (provider === 'openai') {
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey,
|
||||||
|
dangerouslyAllowBrowser: true,
|
||||||
|
});
|
||||||
|
|
||||||
if (!raw) {
|
const stream = await openai.chat.completions.create({
|
||||||
return; // No key to migrate
|
model: model,
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: 'You are a helpful assistant.' },
|
||||||
|
{ role: "user", content: userPrompt },
|
||||||
|
],
|
||||||
|
stream: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
const content = chunk.choices[0]?.delta?.content || "";
|
||||||
|
partial += content;
|
||||||
|
onToken(partial, false);
|
||||||
}
|
}
|
||||||
|
} else if (provider === 'anthropic') {
|
||||||
|
const anthropic = new Anthropic({
|
||||||
|
apiKey,
|
||||||
|
dangerouslyAllowBrowser: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Check if it's already in new format
|
const stream = await anthropic.messages.create({
|
||||||
if (raw.startsWith('{')) {
|
model: model,
|
||||||
try {
|
max_tokens: 4096,
|
||||||
const parsed = JSON.parse(raw);
|
messages: [
|
||||||
if (parsed.keys && (parsed.keys.openai || parsed.keys.anthropic)) {
|
{ role: "user", content: userPrompt }
|
||||||
return; // Already migrated
|
],
|
||||||
}
|
stream: true,
|
||||||
} catch (e) {
|
});
|
||||||
// Continue with migration
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
|
||||||
|
const content = chunk.delta.text || "";
|
||||||
|
partial += content;
|
||||||
|
onToken(partial, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// If it's old format (starts with sk-)
|
throw new Error(`Unsupported provider: ${provider}`)
|
||||||
if (raw.startsWith('sk-')) {
|
|
||||||
// Determine which provider this key belongs to
|
|
||||||
let provider = 'openai';
|
|
||||||
if (raw.startsWith('sk-ant-')) {
|
|
||||||
provider = 'anthropic';
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSettings = {
|
|
||||||
provider: provider,
|
|
||||||
models: {
|
|
||||||
openai: 'gpt-4o',
|
|
||||||
anthropic: 'claude-3-5-sonnet-20241022',
|
|
||||||
google: 'gemini-1.5-flash'
|
|
||||||
},
|
|
||||||
keys: {
|
|
||||||
openai: provider === 'openai' ? raw : '',
|
|
||||||
anthropic: provider === 'anthropic' ? raw : '',
|
|
||||||
google: ''
|
|
||||||
},
|
|
||||||
prompts: {
|
|
||||||
system: 'You are a helpful assistant.'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
localStorage.setItem("openai_api_key", JSON.stringify(newSettings));
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
// Silently handle migration errors
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onToken(partial, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Helper function to get default model for a provider
|
// Helper function to get default model for a provider
|
||||||
function getDefaultModel(provider: string): string {
|
function getDefaultModel(provider: string): string {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
|
|
@ -231,53 +222,75 @@ function getDefaultModel(provider: string): string {
|
||||||
// Helper function to get API key from settings for a specific provider
|
// Helper function to get API key from settings for a specific provider
|
||||||
export function getApiKey(provider: string = 'openai'): string {
|
export function getApiKey(provider: string = 'openai'): string {
|
||||||
try {
|
try {
|
||||||
const settings = localStorage.getItem("openai_api_key")
|
if (provider === 'openai') {
|
||||||
|
return localStorage.getItem("openai_api_key") || "";
|
||||||
if (settings) {
|
} else if (provider === 'anthropic') {
|
||||||
try {
|
return localStorage.getItem("anthropic_api_key") || "";
|
||||||
const parsed = JSON.parse(settings)
|
|
||||||
|
|
||||||
if (parsed.keys && parsed.keys[provider]) {
|
|
||||||
const key = parsed.keys[provider];
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
// Fallback to old format
|
|
||||||
if (typeof settings === 'string' && provider === 'openai') {
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Fallback to old format
|
|
||||||
if (typeof settings === 'string' && provider === 'openai') {
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ""
|
return "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return ""
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to check API key status
|
||||||
|
export function checkApiKeyStatus(): { provider: string; hasValidKey: boolean; keyPreview: string }[] {
|
||||||
|
const results: { provider: string; hasValidKey: boolean; keyPreview: string }[] = [];
|
||||||
|
|
||||||
|
// Check OpenAI key
|
||||||
|
const openaiKey = localStorage.getItem("openai_api_key") || "";
|
||||||
|
const openaiValid = isValidApiKey('openai', openaiKey);
|
||||||
|
results.push({
|
||||||
|
provider: 'openai',
|
||||||
|
hasValidKey: openaiValid,
|
||||||
|
keyPreview: openaiKey ? `${openaiKey.substring(0, 10)}...` : 'empty'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check Anthropic key
|
||||||
|
const anthropicKey = localStorage.getItem("anthropic_api_key") || "";
|
||||||
|
const anthropicValid = isValidApiKey('anthropic', anthropicKey);
|
||||||
|
results.push({
|
||||||
|
provider: 'anthropic',
|
||||||
|
hasValidKey: anthropicValid,
|
||||||
|
keyPreview: anthropicKey ? `${anthropicKey.substring(0, 10)}...` : 'empty'
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to get the first available API key from any provider
|
// Helper function to get the first available API key from any provider
|
||||||
export function getFirstAvailableApiKey(): string | null {
|
export function getFirstAvailableApiKey(): string | null {
|
||||||
try {
|
try {
|
||||||
const settings = localStorage.getItem("openai_api_key")
|
// Try OpenAI first
|
||||||
if (settings) {
|
const openaiKey = localStorage.getItem("openai_api_key");
|
||||||
const parsed = JSON.parse(settings)
|
if (openaiKey && isValidApiKey('openai', openaiKey)) {
|
||||||
if (parsed.keys) {
|
return openaiKey;
|
||||||
for (const [key, value] of Object.entries(parsed.keys)) {
|
|
||||||
if (typeof value === 'string' && value.trim() !== '') {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback to old format
|
|
||||||
if (typeof settings === 'string' && settings.trim() !== '') {
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
|
// Try Anthropic
|
||||||
|
const anthropicKey = localStorage.getItem("anthropic_api_key");
|
||||||
|
if (anthropicKey && isValidApiKey('anthropic', anthropicKey)) {
|
||||||
|
return anthropicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to set up API keys (for debugging/setup)
|
||||||
|
export function setupApiKey(provider: 'openai' | 'anthropic', key: string): void {
|
||||||
|
try {
|
||||||
|
if (provider === 'openai') {
|
||||||
|
localStorage.setItem("openai_api_key", key);
|
||||||
|
} else if (provider === 'anthropic') {
|
||||||
|
localStorage.setItem("anthropic_api_key", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ ${provider} API key set successfully`);
|
||||||
|
console.log(`🔑 Key preview: ${key.substring(0, 10)}...`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("❌ Failed to set API key:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,59 +13,51 @@ import { SlideShape } from "./shapes/SlideShapeUtil"
|
||||||
import { PromptShape } from "./shapes/PromptShapeUtil"
|
import { PromptShape } from "./shapes/PromptShapeUtil"
|
||||||
import { SharedPianoShape } from "./shapes/SharedPianoShapeUtil"
|
import { SharedPianoShape } from "./shapes/SharedPianoShapeUtil"
|
||||||
|
|
||||||
// Lazy load TLDraw dependencies to avoid startup timeouts
|
// Import TLDraw dependencies
|
||||||
let customSchema: any = null
|
import { createTLSchema, defaultBindingSchemas, defaultShapeSchemas } from "@tldraw/tlschema"
|
||||||
let TLSocketRoom: any = null
|
import { TLSocketRoom } from "@tldraw/sync-core"
|
||||||
|
|
||||||
|
// Create custom schema
|
||||||
|
const customSchema = createTLSchema({
|
||||||
|
shapes: {
|
||||||
|
...defaultShapeSchemas,
|
||||||
|
ChatBox: {
|
||||||
|
props: ChatBoxShape.props,
|
||||||
|
migrations: ChatBoxShape.migrations,
|
||||||
|
},
|
||||||
|
VideoChat: {
|
||||||
|
props: VideoChatShape.props,
|
||||||
|
migrations: VideoChatShape.migrations,
|
||||||
|
},
|
||||||
|
Embed: {
|
||||||
|
props: EmbedShape.props,
|
||||||
|
migrations: EmbedShape.migrations,
|
||||||
|
},
|
||||||
|
Markdown: {
|
||||||
|
props: MarkdownShape.props,
|
||||||
|
migrations: MarkdownShape.migrations,
|
||||||
|
},
|
||||||
|
MycrozineTemplate: {
|
||||||
|
props: MycrozineTemplateShape.props,
|
||||||
|
migrations: MycrozineTemplateShape.migrations,
|
||||||
|
},
|
||||||
|
Slide: {
|
||||||
|
props: SlideShape.props,
|
||||||
|
migrations: SlideShape.migrations,
|
||||||
|
},
|
||||||
|
Prompt: {
|
||||||
|
props: PromptShape.props,
|
||||||
|
migrations: PromptShape.migrations,
|
||||||
|
},
|
||||||
|
SharedPiano: {
|
||||||
|
props: SharedPianoShape.props,
|
||||||
|
migrations: SharedPianoShape.migrations,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bindings: defaultBindingSchemas,
|
||||||
|
})
|
||||||
|
|
||||||
async function getTldrawDependencies() {
|
async function getTldrawDependencies() {
|
||||||
if (!customSchema) {
|
|
||||||
const { createTLSchema, defaultBindingSchemas, defaultShapeSchemas } = await import("@tldraw/tlschema")
|
|
||||||
|
|
||||||
customSchema = createTLSchema({
|
|
||||||
shapes: {
|
|
||||||
...defaultShapeSchemas,
|
|
||||||
ChatBox: {
|
|
||||||
props: ChatBoxShape.props,
|
|
||||||
migrations: ChatBoxShape.migrations,
|
|
||||||
},
|
|
||||||
VideoChat: {
|
|
||||||
props: VideoChatShape.props,
|
|
||||||
migrations: VideoChatShape.migrations,
|
|
||||||
},
|
|
||||||
Embed: {
|
|
||||||
props: EmbedShape.props,
|
|
||||||
migrations: EmbedShape.migrations,
|
|
||||||
},
|
|
||||||
Markdown: {
|
|
||||||
props: MarkdownShape.props,
|
|
||||||
migrations: MarkdownShape.migrations,
|
|
||||||
},
|
|
||||||
MycrozineTemplate: {
|
|
||||||
props: MycrozineTemplateShape.props,
|
|
||||||
migrations: MycrozineTemplateShape.migrations,
|
|
||||||
},
|
|
||||||
Slide: {
|
|
||||||
props: SlideShape.props,
|
|
||||||
migrations: SlideShape.migrations,
|
|
||||||
},
|
|
||||||
Prompt: {
|
|
||||||
props: PromptShape.props,
|
|
||||||
migrations: PromptShape.migrations,
|
|
||||||
},
|
|
||||||
SharedPiano: {
|
|
||||||
props: SharedPianoShape.props,
|
|
||||||
migrations: SharedPianoShape.migrations,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
bindings: defaultBindingSchemas,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TLSocketRoom) {
|
|
||||||
const syncCore = await import("@tldraw/sync-core")
|
|
||||||
TLSocketRoom = syncCore.TLSocketRoom
|
|
||||||
}
|
|
||||||
|
|
||||||
return { customSchema, TLSocketRoom }
|
return { customSchema, TLSocketRoom }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue