Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement artifacts poc #513

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/shinkai-desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"app": {
"withGlobalTauri": false,
"security": {
"csp": null
"csp": "default-src 'self' https://2-19-8-sandpack.codesandbox.io"
},
"windows": [
{
Expand Down
148 changes: 148 additions & 0 deletions apps/shinkai-desktop/src/components/chat/artifact-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {
Button,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Tooltip,
TooltipContent,
TooltipPortal,
TooltipProvider,
TooltipTrigger,
} from '@shinkai_network/shinkai-ui';
import { ChevronsRight } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';

import { useChatStore } from './context/chat-context';

const ArtifactPreview = () => {
const artifactCode = useChatStore((state) => state.artifactCode);

const contentRef = useRef<HTMLDivElement>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
const [iframeLoaded, setIframeLoaded] = useState(false);
const toggleArtifactPanel = useChatStore(
(state) => state.toggleArtifactPanel,
);
const handleRender = () => {
if (!iframeRef.current?.contentWindow) return;

iframeRef.current?.contentWindow?.postMessage(
{ type: 'UPDATE_COMPONENT', code: artifactCode },
'*',
);
};
console.log('iframeLoaded', iframeLoaded);

const handleMessage = (event: any) => {
if (event?.data?.type === 'INIT_COMPLETE') {
setIframeLoaded(true);
handleRender();
}
};

useEffect(() => {
window.addEventListener('message', handleMessage);

return () => window.removeEventListener('message', handleMessage);
}, []);

useEffect(() => {
handleRender();
}, [artifactCode]);

return (
<Tabs
className="flex h-screen w-full flex-col overflow-hidden"
defaultValue="source"
>
<div className={'flex h-screen flex-grow justify-stretch p-3'}>
<div className="flex size-full flex-col overflow-hidden">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 px-2">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="text-gray-80 flex items-center gap-2"
onClick={() => toggleArtifactPanel()}
size="icon"
variant="tertiary"
>
<ChevronsRight className="h-4 w-4" />
<span className="sr-only">Close Artifact Panel</span>
</Button>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent className="flex flex-col items-center gap-1">
<p> Close Artifact Panel</p>
</TooltipContent>
</TooltipPortal>
</Tooltip>
</TooltipProvider>
<h1 className="text-sm font-medium text-white">TicTacToe</h1>
</div>
<TabsList className="grid grid-cols-2 rounded-lg border border-gray-400 bg-transparent p-0.5">
<TabsTrigger
className="flex h-8 items-center gap-1.5 text-xs font-semibold"
value="source"
>
Code
</TabsTrigger>
<TabsTrigger
className="flex h-8 items-center gap-1.5 text-xs font-semibold"
value="preview"
>
Preview
</TabsTrigger>
</TabsList>
</div>

<TabsContent
className="mt-1 h-full overflow-y-scroll whitespace-pre-line break-words px-4 py-2 font-mono"
value="source"
>
<SyntaxHighlighter
PreTag="div"
codeTagProps={{
style: {
fontSize: '0.8rem',
fontFamily: 'var(--font-inter)',
},
}}
customStyle={{
margin: 0,
width: '100%',
padding: '0.5rem 1rem',
borderBottomLeftRadius: '8px',
borderBottomRightRadius: '8px',
}}
language={'jsx'}
style={oneDark}
>
{artifactCode}
</SyntaxHighlighter>
</TabsContent>
<TabsContent
className="h-full w-full flex-grow px-4 py-2"
value="preview"
>
<div className="size-full" ref={contentRef}>
<iframe
className="size-full"
loading="lazy"
ref={iframeRef}
src={
'http://localhost:1420/src/windows/shinkai-artifacts/index.html'
}
/>
</div>
</TabsContent>
</div>
</div>
</Tabs>
);
};
export default ArtifactPreview;
68 changes: 68 additions & 0 deletions apps/shinkai-desktop/src/components/chat/context/chat-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createContext, useContext, useState } from 'react';
import { createStore } from 'zustand';
import { useStore } from 'zustand/index';

type ChatStore = {
showArtifactPanel: boolean;
toggleArtifactPanel: () => void;
artifactCode: string;
setArtifactCode: (code: string) => void;
};

const createChatStore = () =>
createStore<ChatStore>((set) => ({
showArtifactPanel: true,
toggleArtifactPanel: () =>
set((state) => ({ showArtifactPanel: !state.showArtifactPanel })),
// remove it later
artifactCode: `
import React from 'react';
import { BarChart, Bar } from 'recharts';

const Chart = () => {
const data = [
{ name: 'Java', value: 10 },
{ name: 'Python', value: 20 },
{ name: 'JavaScript', value: 30 },
{ name: 'C++', value: 15 },
{ name: 'Ruby', value: 5 },
];

return (
<div>
<BarChart width={400} height={300}>
<Bar dataKey="value" fill="#8884d8">
{data.map((entry, index) => (
<Bar key={\`bar-\${index}\`} name={entry.name} />
))}
</Bar>
</BarChart>
</div>
);
};

export default Chart;
`,
// artifactCode: '',
setArtifactCode: (code: string) => set({ artifactCode: code }),
}));

const ChatContext = createContext<ReturnType<typeof createChatStore> | null>(
null,
);

export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
const [store] =
useState<ReturnType<typeof createChatStore>>(createChatStore());

return <ChatContext.Provider value={store}>{children}</ChatContext.Provider>;
};

export function useChatStore<T>(selector: (state: ChatStore) => T) {
const store = useContext(ChatContext);
if (!store) {
throw new Error('Missing ChatProvider');
}
const value = useStore(store, selector);
return value;
}
3 changes: 3 additions & 0 deletions apps/shinkai-desktop/src/pages/chat/chat-conversation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { toast } from 'sonner';

import { streamingSupportedModels } from '../../components/chat/constants';
import { useChatStore } from '../../components/chat/context/chat-context';
import ConversationFooter from '../../components/chat/conversation-footer';
import ConversationHeader from '../../components/chat/conversation-header';
import MessageExtra from '../../components/chat/message-extra';
Expand Down Expand Up @@ -141,6 +142,7 @@ const ChatConversation = () => {
const inboxId = decodeURIComponent(encodedInboxId);
useWebSocketMessage({ inboxId, enabled: true });
useWebSocketTools({ inboxId, enabled: true });
const setArtifactCode = useChatStore((state) => state.setArtifactCode);

const auth = useAuth((state) => state.auth);

Expand Down Expand Up @@ -245,6 +247,7 @@ const ChatConversation = () => {
paginatedMessages={data}
regenerateFirstMessage={regenerateFirstMessage}
regenerateMessage={regenerateMessage}
setArtifactCode={setArtifactCode}
/>

<ConversationFooter />
Expand Down
37 changes: 35 additions & 2 deletions apps/shinkai-desktop/src/pages/chat/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
FormItem,
FormLabel,
Input,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
ScrollArea,
Skeleton,
Tooltip,
Expand All @@ -46,6 +49,8 @@ import { useForm } from 'react-hook-form';
import { Link, Outlet, useMatch, useNavigate } from 'react-router-dom';
import { toast } from 'sonner';

import ArtifactPreview from '../../components/chat/artifact-preview';
import { useChatStore } from '../../components/chat/context/chat-context';
import { useSetJobScope } from '../../components/chat/context/set-job-scope-context';
import { usePromptSelectionStore } from '../../components/prompt/context/prompt-selection-context';
import { useWorkflowSelectionStore } from '../../components/workflow/context/workflow-selection-context';
Expand Down Expand Up @@ -347,6 +352,9 @@ const ChatLayout = () => {
const isChatSidebarCollapsed = useSettings(
(state) => state.isChatSidebarCollapsed,
);

const showArtifactPanel = useChatStore((state) => state.showArtifactPanel);

const navigate = useNavigate();
const auth = useAuth((state) => state.auth);
const resetJobScope = useSetJobScope((state) => state.resetJobScope);
Expand Down Expand Up @@ -383,7 +391,7 @@ const ChatLayout = () => {
{!isChatSidebarCollapsed && (
<motion.div
animate={{ width: 240, opacity: 1 }}
className="flex h-full flex-col overflow-hidden border-r border-gray-300"
className="flex h-full shrink-0 flex-col overflow-hidden border-r border-gray-300"
exit={{ width: 0, opacity: 0 }}
initial={{ width: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
Expand Down Expand Up @@ -499,7 +507,32 @@ const ChatLayout = () => {
</motion.div>
)}
</AnimatePresence>
<Outlet />
<ResizablePanelGroup direction="horizontal">
<ResizablePanel className="flex h-full flex-col">
<Outlet />
</ResizablePanel>
{showArtifactPanel && <ResizableHandle className="bg-gray-300" />}
<ResizablePanel
className={cn(!showArtifactPanel ? 'hidden' : 'block')}
collapsible
defaultSize={64}
maxSize={70}
minSize={40}
>
<AnimatePresence initial={false} mode="popLayout">
{showArtifactPanel && (
<motion.div
animate={{ opacity: 1, filter: 'blur(0px)' }}
className="h-full"
initial={{ opacity: 0, filter: 'blur(5px)' }}
transition={{ duration: 0.2 }}
>
<ArtifactPreview />
</motion.div>
)}
</AnimatePresence>
</ResizablePanel>
</ResizablePanelGroup>
</div>
</TooltipProvider>
);
Expand Down
34 changes: 34 additions & 0 deletions apps/shinkai-desktop/src/pages/chat/preview-system-prompt.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
You are an expert frontend React engineer who is also a great UI/UX designer. Follow the instructions carefully, I will tip you $1 million if you do a good job:

- Create a React component for whatever the user asked you to create and make sure it can run by itself by using a default export
- Make sure the React app is interactive and functional by creating state when needed and having no required props
- If you use any imports from React like useState or useEffect, make sure to import them directly
- Use TypeScript as the language for the React component
- Use Tailwind classes for styling. DO NOT USE ARBITRARY VALUES (e.g. h-[600px]). Make sure to use a consistent color palette.
- Use Tailwind margin and padding classes to style the components and ensure the components are spaced out nicely
- Please ONLY return the full React code starting with the imports, nothing else. It's very important for my job that you only return the React code with imports. DO NOT START WITH
```typescript or ```javascript or ```tsx

. Please only use this when needed.

- Make it everything in single file App.tsx, use raw Reactjs with no extra libraries

Please use the following schema when responding:
{
"title": "string",
"code": "string",
"description": "string",
}

where :
title: "Short title of the fragment. Max 3 words",
description: "Short description of the fragment. Max 1 sentence."
code: "Code generated in jsx. Only runnable code is allowed"


eg:
{
"title": "Tic tac toe",
"code": "import React, { useState } from 'react';\\n\\ninterface BoardSquareProps {\\n value: string | null;\\n}\\n\\nconst BoardSquare = ({ value }: BoardSquareProps) => (\\n <div className=\\"w-20 h-20 rounded-lg text-center flex justify-center items-center m-2 bg-gray-200\\">\\n {value === null ? (\\n <span className=\\"text-4xl\\">{'-'}</span>\\n ) : (\\n <span className=\\"text-4xl\\">{value}</span>\\n )}\\n </div>\\n);\\n\\nconst TicTacToe = () => {\\n const [board, setBoard] = useState<Array<Array<string | null>>>([\\n [null, null, null],\\n [null, null, null],\\n [null, null, null]\\n ]);\\n const [turn, setTurn] = useState<'X' | 'O'>('X');\\n const [winner, setWinner] = useState<string | null>(null);\\n\\n const handleSquareClick = (row: number, col: number) => {\\n if (winner !== null || board[row][col] !== null) return;\\n\\n setBoard(\\n board.map((r, i) =>\\n i === row\\n ? r.map((_, j) => j === col ? turn : _)\\n : r\\n )\\n );\\n setTurn(turn === 'X' ? 'O' : 'X');\\n };\\n\\n const checkWinner = () => {\\n for (let i = 0; i < board.length; i++) {\\n if (\\n board[i][0] !== null &&\\n board[i].every((v) => v === board[i][0])\\n ) return board[i][0];\\n\\n if (\\n board[0][i] !== null &&\\n board.map((r) => r[i]).every((v) => v === board[0][i])\\n ) return board[0][i];\\n }\\n\\n if (board[0][0] !== null && board[0][0] === board[1][1] && board[0][0] === board[2][2]) {\\n return board[0][0];\\n }\\n if (board[0][2] !== null && board[0][2] === board[1][1] && board[0][2] === board[2][0]) {\\n return board[0][2];\\n }\\n\\n for (let i = 0; i < 3; i++) {\\n if (\\n board[i].every((v) => v !== null)\\n ) return 'Tie';\\n }\\n if (board.every((r) => r.some((v) => v === null))) return null;\\n\\n return winner;\\n };\\n\\n React.useEffect(() => {\\n const win = checkWinner();\\n if (win !== null) setWinner(win);\\n }, [board]);\\n\\n return (\\n <div className=\\"flex flex-col items-center\\">\\n {board.map((r, i) => (\\n <div key={i} className=\\"flex justify-center mb-4\\">\\n {r.map((v, j) => (\\n <BoardSquare key={j} value={v} onClick={() => handleSquareClick(i, j)} />\\n ))}\\n </div>\\n ))}\\n </div>\\n );\\n};\\n\\nexport default TicTacToe;",
"description": "Tic tac toe Reactjs app",
}
17 changes: 10 additions & 7 deletions apps/shinkai-desktop/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { debug } from '@tauri-apps/plugin-log';
import React, { useEffect, useRef } from 'react';
import { Navigate, Outlet, Route, Routes, useNavigate } from 'react-router-dom';

import { ChatProvider } from '../components/chat/context/chat-context';
import { SetJobScopeProvider } from '../components/chat/context/set-job-scope-context';
import { ToolsProvider } from '../components/chat/context/tools-context';
import { WalletsProvider } from '../components/crypto-wallet/context/wallets-context';
Expand Down Expand Up @@ -203,13 +204,15 @@ const AppRoutes = () => {
<Route
element={
<ProtectedRoute>
<SetJobScopeProvider>
<WorkflowSelectionProvider>
<PromptSelectionProvider>
<ChatLayout />
</PromptSelectionProvider>
</WorkflowSelectionProvider>
</SetJobScopeProvider>
<ChatProvider>
<SetJobScopeProvider>
<WorkflowSelectionProvider>
<PromptSelectionProvider>
<ChatLayout />
</PromptSelectionProvider>
</WorkflowSelectionProvider>
</SetJobScopeProvider>
</ChatProvider>
</ProtectedRoute>
}
path="inboxes"
Expand Down
Loading