diff --git a/app/components/editor/MonacoEditor.tsx b/app/components/editor/MonacoEditor.tsx new file mode 100644 index 000000000..d643c17b1 --- /dev/null +++ b/app/components/editor/MonacoEditor.tsx @@ -0,0 +1,158 @@ +import { useEffect, useRef, useState } from 'react'; +import type { Theme } from '~/types/theme'; +import type { EditorDocument } from './codemirror/CodeMirrorEditor'; +import type { editor } from 'monaco-editor'; +import { setupMonaco } from '~/lib/monaco-setup'; + +interface MonacoEditorProps { + theme: Theme; + editable?: boolean; + settings?: { + fontSize?: string; + tabSize?: number; + }; + doc?: EditorDocument; + autoFocusOnDocumentChange?: boolean; + onScroll?: (scrollTop: number) => void; + onChange?: (value: string) => void; + onSave?: () => void; +} + +export function MonacoEditor({ + theme, + editable = true, + settings, + doc, + autoFocusOnDocumentChange, + onScroll, + onChange, + onSave, +}: MonacoEditorProps) { + const containerRef = useRef(null); + const editorRef = useRef(); + const [monaco, setMonaco] = useState(); + + useEffect(() => { + // Only run in browser + if (typeof window === 'undefined') return; + + // Load Monaco + import('monaco-editor').then(async (module) => { + await setupMonaco(); + setMonaco(module); + }); + }, []); + + useEffect(() => { + if (!monaco || !containerRef.current) return; + + // Initialize editor + const editor = monaco.editor.create(containerRef.current, { + value: doc?.value, + language: getLanguage(doc?.filePath), + theme: theme === 'dark' ? 'vs-dark' : 'vs', + fontSize: parseInt(settings?.fontSize || '14'), + tabSize: settings?.tabSize || 2, + minimap: { + enabled: false + }, + scrollBeyondLastLine: false, + renderWhitespace: 'boundary', + readOnly: !editable, + lineNumbers: 'on', + wordWrap: 'on', + folding: true, + bracketPairColorization: { + enabled: true + }, + automaticLayout: true, + scrollbar: { + verticalScrollbarSize: 14, + horizontalScrollbarSize: 14 + } + }); + + editorRef.current = editor; + + // Set up VSCode keybindings + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => { + onSave?.(); + }); + + // Set up scroll listener + editor.onDidScrollChange((e) => { + onScroll?.(e.scrollTop); + }); + + // Set up change listener + editor.onDidChangeModelContent(() => { + onChange?.(editor.getValue()); + }); + + return () => { + editor.dispose(); + }; + }, [monaco, containerRef.current]); + + useEffect(() => { + if (!editorRef.current || !doc) return; + + // Update content if needed + if (doc.value !== editorRef.current.getValue()) { + editorRef.current.setValue(doc.value); + } + + // Update language if needed + const model = editorRef.current.getModel(); + if (model) { + const currentLanguage = model.getLanguageId(); + const newLanguage = getLanguage(doc.filePath); + if (currentLanguage !== newLanguage) { + monaco?.editor.setModelLanguage(model, newLanguage); + } + } + }, [doc?.value, doc?.filePath]); + + useEffect(() => { + if (!editorRef.current || !monaco) return; + monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'vs'); + }, [theme]); + + useEffect(() => { + if (editorRef.current && autoFocusOnDocumentChange) { + editorRef.current.focus(); + } + }, [doc, autoFocusOnDocumentChange]); + + // Determine language from file extension + const getLanguage = (filePath?: string) => { + if (!filePath) return 'plaintext'; + const ext = filePath.split('.').pop()?.toLowerCase(); + switch (ext) { + case 'js': + return 'javascript'; + case 'jsx': + return 'javascript'; + case 'ts': + return 'typescript'; + case 'tsx': + return 'typescript'; + case 'json': + return 'json'; + case 'md': + return 'markdown'; + case 'css': + return 'css'; + case 'scss': + return 'scss'; + case 'html': + return 'html'; + case 'py': + return 'python'; + default: + return 'plaintext'; + } + }; + + return
; +} diff --git a/app/components/workbench/EditorPanel.tsx b/app/components/workbench/EditorPanel.tsx index 9a2dd4f13..d6d54a7f3 100644 --- a/app/components/workbench/EditorPanel.tsx +++ b/app/components/workbench/EditorPanel.tsx @@ -1,14 +1,10 @@ import { useStore } from '@nanostores/react'; import { memo, useEffect, useMemo, useRef, useState } from 'react'; import { Panel, PanelGroup, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels'; -import { - CodeMirrorEditor, - type EditorDocument, - type EditorSettings, - type OnChangeCallback as OnEditorChange, - type OnSaveCallback as OnEditorSave, - type OnScrollCallback as OnEditorScroll, -} from '~/components/editor/codemirror/CodeMirrorEditor'; +import type { EditorDocument, EditorSettings } from '~/components/editor/codemirror/CodeMirrorEditor'; +import type { OnChangeCallback as OnEditorChange } from '~/components/editor/codemirror/CodeMirrorEditor'; +import type { OnSaveCallback as OnEditorSave } from '~/components/editor/codemirror/CodeMirrorEditor'; +import type { OnScrollCallback as OnEditorScroll } from '~/components/editor/codemirror/CodeMirrorEditor'; import { IconButton } from '~/components/ui/IconButton'; import { PanelHeader } from '~/components/ui/PanelHeader'; import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton'; @@ -23,6 +19,7 @@ import { isMobile } from '~/utils/mobile'; import { FileBreadcrumb } from './FileBreadcrumb'; import { FileTree } from './FileTree'; import { Terminal, type TerminalRef } from './terminal/Terminal'; +import { EditorSelection } from '@codemirror/state'; interface EditorPanelProps { files?: FileMap; @@ -69,11 +66,19 @@ export const EditorPanel = memo( const terminalRefs = useRef>([]); const terminalPanelRef = useRef(null); const terminalToggledByShortcut = useRef(false); + const [Editor, setEditor] = useState(null); const [activeTerminal, setActiveTerminal] = useState(0); const [terminalCount, setTerminalCount] = useState(1); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + useEffect(() => { + // Dynamically import Monaco editor on client-side only + import('~/components/editor/MonacoEditor').then((module) => { + setEditor(() => module.MonacoEditor); + }); + }, []); + const activeFileSegments = useMemo(() => { if (!editorDocument) { return undefined; @@ -128,6 +133,18 @@ export const EditorPanel = memo( } }; + // Adapter functions to convert between CodeMirror and Monaco callback types + const handleEditorScroll = (scrollTop: number) => { + onEditorScroll?.({ top: scrollTop, left: 0 }); + }; + + const handleEditorChange = (value: string) => { + onEditorChange?.({ + selection: EditorSelection.single(0), + content: value + }); + }; + return ( @@ -180,16 +197,20 @@ export const EditorPanel = memo( )}
- + {Editor ? ( + + ) : ( +
Loading editor...
+ )}
diff --git a/app/lib/monaco-setup.ts b/app/lib/monaco-setup.ts new file mode 100644 index 000000000..6d53cd334 --- /dev/null +++ b/app/lib/monaco-setup.ts @@ -0,0 +1,238 @@ +import type * as monaco from 'monaco-editor'; + +// Configure Monaco editor with VSCode-like settings +export async function setupMonaco() { + // Only run in browser + if (typeof window === 'undefined') return; + + const monaco = await import('monaco-editor'); + + // Configure editor defaults for dark theme (VSCode Dark+) + monaco.editor.defineTheme('vs-dark', { + base: 'vs-dark', + inherit: false, + rules: [ + // Base tokens + { token: '', foreground: 'd4d4d4' }, + { token: 'invalid', foreground: 'f44747' }, + { token: 'emphasis', fontStyle: 'italic' }, + { token: 'strong', fontStyle: 'bold' }, + + // Comments + { token: 'comment', foreground: '6A9955', fontStyle: 'italic' }, + { token: 'comment.content', foreground: '6A9955' }, + + // Variables + { token: 'variable', foreground: '9CDCFE' }, + { token: 'variable.predefined', foreground: '4FC1FF' }, + { token: 'variable.parameter', foreground: '9CDCFE' }, + + // Keywords + { token: 'keyword', foreground: '569CD6' }, + { token: 'keyword.control', foreground: 'C586C0' }, + { token: 'keyword.operator', foreground: 'D4D4D4' }, + + // Types + { token: 'type', foreground: '4EC9B0' }, + { token: 'type.identifier', foreground: '4EC9B0' }, + { token: 'type.parameter', foreground: '4EC9B0' }, + { token: 'type.enum', foreground: '4EC9B0' }, + { token: 'type.interface', foreground: '4EC9B0' }, + + // Functions + { token: 'function', foreground: 'DCDCAA' }, + { token: 'function.declaration', foreground: 'DCDCAA' }, + + // Classes + { token: 'class', foreground: '4EC9B0' }, + { token: 'class.declaration', foreground: '4EC9B0' }, + + // Strings + { token: 'string', foreground: 'CE9178' }, + { token: 'string.escape', foreground: 'D7BA7D' }, + + // Numbers + { token: 'number', foreground: 'B5CEA8' }, + { token: 'number.hex', foreground: 'B5CEA8' }, + + // Properties + { token: 'property', foreground: '9CDCFE' }, + { token: 'property.declaration', foreground: '9CDCFE' }, + + // Tags and markup + { token: 'tag', foreground: '569CD6' }, + { token: 'tag.attribute.name', foreground: '9CDCFE' }, + { token: 'tag.attribute.value', foreground: 'CE9178' }, + + // Punctuation + { token: 'delimiter', foreground: 'D4D4D4' }, + { token: 'delimiter.html', foreground: '808080' }, + { token: 'delimiter.xml', foreground: '808080' }, + + // Special tokens + { token: 'namespace', foreground: '4EC9B0' }, + { token: 'regex', foreground: 'D16969' }, + { token: 'annotation', foreground: 'DCDCAA' }, + { token: 'constant', foreground: '4FC1FF' } + ], + colors: { + 'editor.background': '#1E1E1E', + 'editor.foreground': '#D4D4D4', + 'editor.lineHighlightBackground': '#2F2F2F', + 'editor.selectionBackground': '#264F78', + 'editor.inactiveSelectionBackground': '#3A3D41', + 'editor.selectionHighlightBackground': '#ADD6FF26', + 'editor.wordHighlightBackground': '#575757B8', + 'editor.wordHighlightStrongBackground': '#004972B8', + 'editor.findMatchBackground': '#515C6A', + 'editor.findMatchHighlightBackground': '#EA5C0055', + + 'editorLineNumber.foreground': '#858585', + 'editorLineNumber.activeForeground': '#C6C6C6', + + 'editorIndentGuide.background': '#404040', + 'editorIndentGuide.activeBackground': '#707070', + + 'editorBracketMatch.background': '#0064001A', + 'editorBracketMatch.border': '#888888', + + 'editorOverviewRuler.border': '#7F7F7F4D', + 'editorOverviewRuler.findMatchForeground': '#D18616', + 'editorOverviewRuler.rangeHighlightForeground': '#007ACC99', + + 'scrollbarSlider.background': '#79797966', + 'scrollbarSlider.hoverBackground': '#646464B3', + 'scrollbarSlider.activeBackground': '#BFBFBF66' + } + }); + + // Configure editor defaults for light theme (VSCode Light) + monaco.editor.defineTheme('vs', { + base: 'vs', + inherit: false, + rules: [ + // Base tokens + { token: '', foreground: '000000' }, + { token: 'invalid', foreground: 'cd3131' }, + { token: 'emphasis', fontStyle: 'italic' }, + { token: 'strong', fontStyle: 'bold' }, + + // Comments + { token: 'comment', foreground: '008000', fontStyle: 'italic' }, + { token: 'comment.content', foreground: '008000' }, + + // Variables + { token: 'variable', foreground: '001080' }, + { token: 'variable.predefined', foreground: '0070C1' }, + { token: 'variable.parameter', foreground: '001080' }, + + // Keywords + { token: 'keyword', foreground: '0000FF' }, + { token: 'keyword.control', foreground: 'AF00DB' }, + { token: 'keyword.operator', foreground: '000000' }, + + // Types + { token: 'type', foreground: '267F99' }, + { token: 'type.identifier', foreground: '267F99' }, + { token: 'type.parameter', foreground: '267F99' }, + { token: 'type.enum', foreground: '267F99' }, + { token: 'type.interface', foreground: '267F99' }, + + // Functions + { token: 'function', foreground: '795E26' }, + { token: 'function.declaration', foreground: '795E26' }, + + // Classes + { token: 'class', foreground: '267F99' }, + { token: 'class.declaration', foreground: '267F99' }, + + // Strings + { token: 'string', foreground: 'A31515' }, + { token: 'string.escape', foreground: 'FF0000' }, + + // Numbers + { token: 'number', foreground: '098658' }, + { token: 'number.hex', foreground: '098658' }, + + // Properties + { token: 'property', foreground: '001080' }, + { token: 'property.declaration', foreground: '001080' }, + + // Tags and markup + { token: 'tag', foreground: '800000' }, + { token: 'tag.attribute.name', foreground: 'FF0000' }, + { token: 'tag.attribute.value', foreground: '0000FF' }, + + // Punctuation + { token: 'delimiter', foreground: '000000' }, + { token: 'delimiter.html', foreground: '808080' }, + { token: 'delimiter.xml', foreground: '808080' }, + + // Special tokens + { token: 'namespace', foreground: '267F99' }, + { token: 'regex', foreground: '811F3F' }, + { token: 'annotation', foreground: '808080' }, + { token: 'constant', foreground: '0070C1' } + ], + colors: { + 'editor.background': '#FFFFFF', + 'editor.foreground': '#000000', + 'editor.lineHighlightBackground': '#F8F8F8', + 'editor.selectionBackground': '#ADD6FF', + 'editor.inactiveSelectionBackground': '#E5EBF1', + 'editor.selectionHighlightBackground': '#ADD6FF80', + 'editor.wordHighlightBackground': '#57575740', + 'editor.wordHighlightStrongBackground': '#0E639C40', + 'editor.findMatchBackground': '#A8AC94', + 'editor.findMatchHighlightBackground': '#EA5C0055', + + 'editorLineNumber.foreground': '#237893', + 'editorLineNumber.activeForeground': '#237893', + + 'editorIndentGuide.background': '#D3D3D3', + 'editorIndentGuide.activeBackground': '#939393', + + 'editorBracketMatch.background': '#0064001A', + 'editorBracketMatch.border': '#B9B9B9', + + 'editorOverviewRuler.border': '#7F7F7F4D', + 'editorOverviewRuler.findMatchForeground': '#D18616', + 'editorOverviewRuler.rangeHighlightForeground': '#007ACC99', + + 'scrollbarSlider.background': '#64646466', + 'scrollbarSlider.hoverBackground': '#646464B3', + 'scrollbarSlider.activeBackground': '#00000033' + } + }); + + // Configure TypeScript/JavaScript settings + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + target: monaco.languages.typescript.ScriptTarget.Latest, + allowNonTsExtensions: true, + moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + module: monaco.languages.typescript.ModuleKind.CommonJS, + noEmit: true, + esModuleInterop: true, + jsx: monaco.languages.typescript.JsxEmit.React, + reactNamespace: 'React', + allowJs: true, + typeRoots: ['node_modules/@types'] + }); + + // Set formatting options + monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ + noSemanticValidation: false, + noSyntaxValidation: false + }); + + // Configure JavaScript settings + monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + target: monaco.languages.typescript.ScriptTarget.Latest, + allowNonTsExtensions: true, + allowJs: true, + checkJs: true + }); + + // Set default theme + monaco.editor.setTheme('vs-dark'); +} diff --git a/app/routes/test.tsx b/app/routes/test.tsx new file mode 100644 index 000000000..91f50e0c8 --- /dev/null +++ b/app/routes/test.tsx @@ -0,0 +1,118 @@ +import { useStore } from '@nanostores/react'; +import { themeStore } from '~/lib/stores/theme'; +import { useEffect, useState } from 'react'; + +export default function TestPage() { + const theme = useStore(themeStore); + const [Editor, setEditor] = useState(null); + + useEffect(() => { + // Dynamically import Monaco editor on client-side only + import('~/components/editor/MonacoEditor').then((module) => { + setEditor(() => module.MonacoEditor); + }); + }, []); + + const testContent = `// Test file to verify VSCode syntax highlighting + +// Keywords and control flow +import React, { useState, useEffect } from 'react'; +const test = true; +if (test) { + console.log('Testing'); +} + +// Types and interfaces +interface User { + id: number; + name: string; + isActive: boolean; +} + +type UserRole = 'admin' | 'user' | 'guest'; + +// Class definition +class UserManager { + private users: User[] = []; + + constructor() { + this.users = []; + } + + // Method with parameters + public addUser(user: User): void { + this.users.push(user); + } +} + +// Function with string template +function formatUser(user: User): string { + return \`User \${user.name} (ID: \${user.id})\`; +} + +// React component with hooks +export function TestComponent() { + const [count, setCount] = useState(0); + const [user, setUser] = useState(null); + + useEffect(() => { + // Comments should be green + const manager = new UserManager(); + manager.addUser({ + id: 1, + name: "Test User", + isActive: true + }); + }, []); + + // JSX with attributes + return ( +
+

Test Component

+ + {user &&

{formatUser(user)}

} +
+ ); +} + +// Regular expressions +const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/; +const testEmail = "test@example.com"; +const isValidEmail = emailRegex.test(testEmail); + +// Object with different value types +const config = { + apiUrl: 'https://api.example.com', + maxRetries: 3, + timeout: 5000, + features: { + darkMode: true, + notifications: false + } +}; + +export default TestComponent;`; + + if (!Editor) { + return
Loading editor...
; + } + + return ( +
+ +
+ ); +} diff --git a/app/test-syntax.tsx b/app/test-syntax.tsx new file mode 100644 index 000000000..8af135bdf --- /dev/null +++ b/app/test-syntax.tsx @@ -0,0 +1,81 @@ +// Test file to verify VSCode syntax highlighting + +// Keywords and control flow +import React, { useState, useEffect } from 'react'; +const test = true; +if (test) { + console.log('Testing'); +} + +// Types and interfaces +interface User { + id: number; + name: string; + isActive: boolean; +} + +type UserRole = 'admin' | 'user' | 'guest'; + +// Class definition +class UserManager { + private users: User[] = []; + + constructor() { + this.users = []; + } + + // Method with parameters + public addUser(user: User): void { + this.users.push(user); + } +} + +// Function with string template +function formatUser(user: User): string { + return `User ${user.name} (ID: ${user.id})`; +} + +// React component with hooks +export function TestComponent() { + const [count, setCount] = useState(0); + const [user, setUser] = useState(null); + + useEffect(() => { + // Comments should be green + const manager = new UserManager(); + manager.addUser({ + id: 1, + name: "Test User", + isActive: true + }); + }, []); + + // JSX with attributes + return ( +
+

Test Component

+ + {user &&

{formatUser(user)}

} +
+ ); +} + +// Regular expressions +const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; +const testEmail = "test@example.com"; +const isValidEmail = emailRegex.test(testEmail); + +// Object with different value types +const config = { + apiUrl: 'https://api.example.com', + maxRetries: 3, + timeout: 5000, + features: { + darkMode: true, + notifications: false + } +}; + +export default TestComponent; diff --git a/package-lock.json b/package-lock.json index abcf43b91..221f9373f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@iconify-json/ph": "^1.1.13", "@iconify-json/svg-spinners": "^1.1.2", "@lezer/highlight": "^1.2.0", + "@monaco-editor/react": "^4.6.0", "@nanostores/react": "^0.7.2", "@octokit/rest": "^21.0.2", "@octokit/types": "^13.6.1", @@ -53,6 +54,7 @@ "istextorbinary": "^9.5.0", "jose": "^5.6.3", "jszip": "^3.10.1", + "monaco-editor": "^0.52.0", "nanostores": "^0.10.3", "ollama-ai-provider": "^0.15.2", "react": "^18.2.0", @@ -2646,6 +2648,32 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@nanostores/react": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/@nanostores/react/-/react-0.7.3.tgz", @@ -17179,6 +17207,12 @@ "dev": true, "license": "MIT" }, + "node_modules/monaco-editor": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "license": "MIT" + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -21601,6 +21635,12 @@ "get-source": "^2.0.12" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index edb2b8dad..59ffb9459 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "dependencies": { "@ai-sdk/anthropic": "^0.0.39", "@ai-sdk/google": "^0.0.52", - "@ai-sdk/openai": "^0.0.66", "@ai-sdk/mistral": "^0.0.43", + "@ai-sdk/openai": "^0.0.66", "@codemirror/autocomplete": "^6.17.0", "@codemirror/commands": "^6.6.0", "@codemirror/lang-cpp": "^6.0.2", @@ -44,6 +44,7 @@ "@iconify-json/ph": "^1.1.13", "@iconify-json/svg-spinners": "^1.1.2", "@lezer/highlight": "^1.2.0", + "@monaco-editor/react": "^4.6.0", "@nanostores/react": "^0.7.2", "@octokit/rest": "^21.0.2", "@octokit/types": "^13.6.1", @@ -68,6 +69,7 @@ "istextorbinary": "^9.5.0", "jose": "^5.6.3", "jszip": "^3.10.1", + "monaco-editor": "^0.52.0", "nanostores": "^0.10.3", "ollama-ai-provider": "^0.15.2", "react": "^18.2.0", diff --git a/test.tsx b/test.tsx new file mode 100644 index 000000000..44a22993c --- /dev/null +++ b/test.tsx @@ -0,0 +1,38 @@ +// This is a comment +import React from 'react'; + +interface Props { + name: string; + age: number; + isActive?: boolean; +} + +const colors = { + primary: '#ff0000', + secondary: '#00ff00' +}; + +export class TestComponent extends React.Component { + private count: number = 0; + + handleClick = () => { + this.count += 1; + console.log(`Count is now ${this.count}`); + } + + render() { + const { name, age, isActive = false } = this.props; + + return ( +
+

Hello {name}!

+

You are {age} years old

+ {isActive && ( + + )} +
+ ); + } +}