Spaces:
Running
Running
File size: 7,524 Bytes
24137a8 531624f c5b6c1c 0c41143 95e364f c5b6c1c 531624f 77f0b2e 531624f 1cca192 531624f c5b6c1c 531624f f1f6d9b a12df23 f1f6d9b a12df23 f1f6d9b c5b6c1c f1f6d9b c5b6c1c f1f6d9b c5b6c1c f1f6d9b 531624f c5b6c1c d200fc9 c5b6c1c d200fc9 6bd3265 d200fc9 6bd3265 d200fc9 c5b6c1c 531624f c5b6c1c a12df23 c5b6c1c 77f0b2e c5b6c1c 531624f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
import React, { useEffect, useState, Component, type ErrorInfo } from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { NoteViewer } from '@/components/NoteViewer';
import { SearchWidget } from '@/components/SearchWidget';
import type { Note } from '@/types/note';
import type { SearchResult } from '@/types/search';
import { getNote } from '@/services/api';
import { Loader2, AlertTriangle } from 'lucide-react';
// Mock window.openai for development
if (!window.openai) {
console.warn("Mocking window.openai (dev mode)");
window.openai = {
toolOutput: null
};
}
// Extend window interface
declare global {
interface Window {
openai: {
toolOutput: any;
toolInput?: any;
};
}
}
class WidgetErrorBoundary extends Component<{ children: React.ReactNode }, { hasError: boolean, error: Error | null }> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Widget Error:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="min-h-screen bg-background text-destructive p-4 flex flex-col items-center justify-center text-center">
<AlertTriangle className="h-8 w-8 mb-2" />
<h2 className="font-bold text-lg">Something went wrong</h2>
<p className="text-sm text-muted-foreground mt-1 max-w-xs">
{this.state.error?.message || "The widget could not be displayed."}
</p>
</div>
);
}
return this.props.children;
}
}
const WidgetApp = () => {
const [view, setView] = useState<'loading' | 'note' | 'search' | 'error'>('loading');
const [data, setData] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Function to check for data
const checkData = () => {
const toolOutput = window.openai.toolOutput;
if (toolOutput) {
console.log("Tool output found:", toolOutput);
if (toolOutput.note) {
setView('note');
setData(toolOutput.note);
} else if (toolOutput.results) {
setView('search');
setData(toolOutput.results);
} else {
console.warn("Unknown tool output format", toolOutput);
setError("Unknown content format.");
setView('error');
}
return true;
}
return false;
};
// Check immediately
if (checkData()) return;
// If not found, check if we have toolInput (loading state)
// Note: window.openai types might vary, we check safely
const toolInput = (window.openai as any).toolInput;
if (toolInput) {
console.log("Tool input present, waiting for output:", toolInput);
setView('loading');
} else {
// Fallback for dev testing via URL (only if no toolInput either)
const params = new URLSearchParams(window.location.search);
const mockType = params.get('type');
if (mockType === 'note') {
// ... existing mock logic ...
setView('note');
setData({
title: "Demo Note",
note_path: "demo.md",
body: "# Demo Note\n\nMock note.",
version: 1,
size_bytes: 100,
created: new Date().toISOString(),
updated: new Date().toISOString(),
metadata: {}
});
return;
} else if (mockType === 'search') {
setView('search');
setData([{ title: "Result 1", note_path: "res1.md", snippet: "Match", score: 1, updated: new Date() }]);
return;
}
// If absolutely nothing, show error (or keep loading if we suspect latency)
// But sticking to 'loading' is safer than error if we are polling.
}
// Poll for data injection
const interval = setInterval(() => {
if (checkData()) clearInterval(interval);
}, 500);
return () => clearInterval(interval);
}, []);
const handleWikilinkClick = (linkText: string) => {
console.log("Clicked wikilink in widget:", linkText);
// Convert wikilink text to path (simple slugification or just try reading it)
// We can try to fetch it directly.
// Reuse handleNoteSelect logic but we need to slugify first?
// For now, let's try strict filename matching or just pass text.
// getNote expects a path.
// We'll just alert for now or try to resolve it.
// Let's try to fetch it as a path (assuming user clicked "Getting Started")
handleNoteSelect(linkText + ".md");
};
const handleNoteSelect = async (path: string) => {
console.log("Selected note:", path);
setView('loading');
try {
const note = await getNote(path);
setData(note);
setView('note');
} catch (err: any) {
console.error("Failed to load note:", err);
const msg = err?.message || String(err);
const status = err?.status ? ` (Status: ${err.status})` : '';
setError(`Failed to load note: ${path}\nError: ${msg}${status}`);
setView('error');
}
};
return (
<div className="dark min-h-screen bg-background text-foreground flex flex-col">
{view === 'loading' && (
<div className="flex-1 flex flex-col items-center justify-center p-4 text-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground mb-4" />
<p className="text-sm text-muted-foreground">
{window.openai?.toolInput?.query
? `Searching for "${window.openai.toolInput.query}"...`
: "Waiting for tool execution..."}
</p>
<p className="text-xs text-muted-foreground mt-2">
(Please confirm the action in ChatGPT)
</p>
</div>
)}
{view === 'error' && (
<div className="p-4 text-destructive flex flex-col gap-4 overflow-auto">
<div>
<h2 className="font-bold">Error</h2>
<p>{error}</p>
</div>
<div className="bg-muted p-2 rounded text-xs font-mono text-foreground">
<p className="font-bold mb-1">Debug Info (window.openai):</p>
<pre className="whitespace-pre-wrap break-all">
{JSON.stringify(window.openai, null, 2)}
</pre>
</div>
<button
className="px-4 py-2 bg-primary text-primary-foreground rounded hover:opacity-90"
onClick={() => window.location.reload()}
>
Reload Widget
</button>
</div>
)}
{view === 'note' && data && (
<NoteViewer
note={data as Note}
backlinks={[]} // Backlinks usually fetched separately, omit for V1 widget
onWikilinkClick={handleWikilinkClick}
/>
)}
{view === 'search' && data && (
<SearchWidget
results={data as SearchResult[]}
onSelectNote={handleNoteSelect}
/>
)}
</div>
);
};
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<WidgetErrorBoundary>
<WidgetApp />
</WidgetErrorBoundary>
</React.StrictMode>
);
|