diff --git a/README-UPDATE-SCRIPT.md b/README-UPDATE-SCRIPT.md new file mode 100644 index 0000000..5df5134 --- /dev/null +++ b/README-UPDATE-SCRIPT.md @@ -0,0 +1,187 @@ +# MoreMinimore Update and Debranding Script + +This script (`scripts/update-and-debrand.sh`) automatically applies all custom features and removes Dyad branding/dependencies from the original Dyad codebase, converting it into MoreMinimore. + +## πŸš€ Features Applied + +### Custom Features +- βœ… **Remove-limit feature** - Removes all usage limitations and restrictions +- βœ… **Smart Context for all users** - Previously Pro-only feature now available to everyone +- βœ… **Annotator tool for all users** - Previously Pro-only feature now available to everyone +- βœ… **No Pro upgrade buttons** - All Pro restrictions and upgrade prompts removed + +### Debranding Changes +- βœ… **Dyad β†’ MoreMinimore** - Complete branding transformation +- βœ… **Component names updated** - All Dyad* components renamed to MoreMinimore* +- βœ… **URLs updated** - All dyad.sh links changed to moreminimore.com +- βœ… **Protocol handlers** - Custom `moreminimore://` protocol instead of `dyad://` +- βœ… **App metadata** - Title bar and app information updated + +### API Dependencies Removed +- βœ… **Dyad Template API** - Uses local templates only +- βœ… **Dyad Release Notes API** - Release notes check disabled +- βœ… **Dyad Auto-update API** - Auto-update functionality removed +- βœ… **Dyad Engine dependencies** - All Dyad Engine references removed + +### UI/UX Improvements +- βœ… **YouTube video section removed** - Cleaner setup experience +- βœ… **ProBanner commented out** - No Pro promotion banners +- βœ… **ProModeSelector removed** - Simplified chat controls +- βœ… **Context Settings button** - Improved UI text + +## πŸ“‹ Usage + +### Quick Start +```bash +# Make the script executable +chmod +x scripts/update-and-debrand.sh + +# Run the script +./scripts/update-and-debrand.sh +``` + +### What the Script Does +1. **Creates backup** - Automatic backup before making changes +2. **Applies custom features** - Enables remove-limit and other features +3. **Removes Dyad dependencies** - Eliminates all external API calls +4. **Updates branding** - Complete Dyad β†’ MoreMinimore transformation +5. **Installs dependencies** - Updates package dependencies +6. **Tests compilation** - Verifies TypeScript compilation + +### Safety Features +- βœ… **Automatic backup** - Creates timestamped backup directory +- βœ… **Error handling** - Script exits on any error +- βœ… **Compilation test** - Verifies code compiles successfully +- βœ… **Dependency check** - Ensures all dependencies are installed + +## πŸ”„ Update Workflow + +When you want to update your MoreMinimore with new features from the original Dyad repository: + +1. **Pull latest changes** from upstream Dyad repository +2. **Run the script** to re-apply all customizations: + ```bash + ./scripts/update-and-debrand.sh + ``` +3. **Test the application** to ensure everything works: + ```bash + npm start + ``` +4. **Commit changes** if everything works correctly + +## πŸ“ Backup System + +The script creates automatic backups with timestamp: +``` +dyad-backup-YYYYMMDD-HHMMSS/ +β”œβ”€β”€ src/ # Source code backup +β”œβ”€β”€ package.json # Package configuration +└── scripts/ # Scripts backup +``` + +## πŸ› οΈ Technical Details + +### Files Modified +- `src/custom/index.ts` - Custom feature flags +- `src/ipc/utils/template_utils.ts` - Template API removal +- `src/ipc/handlers/release_note_handlers.ts` - Release notes API removal +- `src/main.ts` - Auto-update and protocol handler updates +- `src/ipc/utils/get_model_client.ts` - Dyad Engine removal +- `src/ipc/ipc_host.ts` - Pro handlers removal +- `src/preload.ts` - Pro IPC channels removal +- `src/components/ChatInputControls.tsx` - ProModeSelector removal +- `src/pages/home.tsx` - ProBanner commenting +- `src/ipc/utils/smart_context_store.ts` - Smart context liberation +- `src/ipc/shared/language_model_constants.ts` - Provider updates +- `src/components/SetupBanner.tsx` - YouTube section removal +- `src/app/TitleBar.tsx` - Title bar branding +- `package.json` - App description +- All component files - Dyad β†’ MoreMinimore renaming +- All files - URL updates and branding text + +### Key Features Liberated +1. **Smart Context** - Advanced context management for all users +2. **Annotator Tool** - Visual editing and annotation capabilities +3. **Unlimited Usage** - No message limits or restrictions +4. **Full Feature Access** - All Pro features available to everyone + +## 🎯 Benefits + +### For Users +- **No limitations** - Unlimited access to all features +- **Better privacy** - No external API calls to Dyad services +- **Full functionality** - All Pro features available +- **Clean interface** - No upgrade prompts or restrictions + +### For Developers +- **Easy updates** - Single script to re-apply all changes +- **Safe modifications** - Automatic backup system +- **Maintainable** - Clear separation of custom features +- **Extensible** - Easy to add new custom features + +## πŸ”§ Troubleshooting + +### Common Issues + +#### TypeScript Compilation Errors +```bash +# Check TypeScript compilation +npm run ts + +# If errors occur, check the backup and restore if needed +cp -r dyad-backup-YYYYMMDD-HHMMSS/src ./src +``` + +#### Application Won't Start +```bash +# Check dependencies +npm install + +# Rebuild the application +npm run rebuild + +# Check logs for specific errors +npm start +``` + +#### Script Permissions +```bash +# Make script executable +chmod +x scripts/update-and-debrand.sh +``` + +### Manual Recovery +If something goes wrong, you can always restore from the backup: +```bash +# Find your backup directory +ls dyad-backup-* + +# Restore from backup +cp -r dyad-backup-YYYYMMDD-HHMMSS/src ./src +cp dyad-backup-YYYYMMDD-HHMMSS/package.json ./package.json +``` + +## πŸ“ Customization + +### Adding New Custom Features +1. Add your feature flag to `src/custom/index.ts` +2. Implement the feature in the appropriate files +3. Update the script to apply your changes automatically + +### Modifying the Script +The script is organized into clear functions: +- `apply_custom_features()` - Apply custom modifications +- `remove_dyad_apis()` - Remove external dependencies +- `update_branding()` - Update branding and names +- `remove_pro_features()` - Remove Pro restrictions + +## πŸŽ‰ Result + +After running the script, you'll have: +- **MoreMinimore** - Fully branded application +- **No limitations** - All features available to all users +- **No external dependencies** - Complete independence from Dyad APIs +- **Clean interface** - No Pro upgrade prompts or restrictions +- **Easy updates** - Simple script to re-apply changes + +The application will be ready to use with all custom features enabled and no Dyad branding or dependencies. diff --git a/package.json b/package.json index dddaf3d..3f6e99e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "moreminimore", "productName": "moreminimore", "version": "0.31.0-beta.1", - "description": "Free, local, open-source AI app builder", + "description": "MoreMinimore - AI-powered development environment", "main": ".vite/build/main.js", "repository": { "type": "git", diff --git a/scripts/update-and-debrand.sh b/scripts/update-and-debrand.sh index fea60e3..d9c7d19 100755 --- a/scripts/update-and-debrand.sh +++ b/scripts/update-and-debrand.sh @@ -156,15 +156,8 @@ remove_pro_features() { # Remove Pro restrictions from PreviewIframe (Annotator) if [ -f "src/components/preview_panel/PreviewIframe.tsx" ]; then - sed -i.bak '/import { AnnotatorOnlyForPro } from ".\/AnnotatorOnlyForPro";/d' src/components/preview_panel/PreviewIframe.tsx - sed -i.bak '/{userBudget ? (/,/)} : (/,//c\ - ' src/components/preview_panel/PreviewIframe.tsx - rm src/components/preview_panel/PreviewIframe.tsx.bak - print_success "Removed Pro restrictions from Annotator" + # The file already uses Annotator directly, no Pro restrictions to remove + print_success "Annotator already available for all users" fi # Comment out ProBanner in home.tsx @@ -314,6 +307,18 @@ remove_youtube_section() { fi } +# Function to fix ChatInput.tsx references +fix_chat_input() { + print_status "Fixing ChatInput.tsx references..." + + if [ -f "src/components/chat/ChatInput.tsx" ]; then + # Update the Pro URL + sed -i.bak 's|https://dyad.sh/pro|https://moreminimore.com/pro|g' src/components/chat/ChatInput.tsx + rm src/components/chat/ChatInput.tsx.bak + print_success "Fixed ChatInput.tsx references" + fi +} + # Function to update title bar and app metadata update_app_metadata() { print_status "Updating app metadata..." @@ -400,6 +405,7 @@ main() { update_branding_text update_ai_providers remove_youtube_section + fix_chat_input update_app_metadata cleanup_imports install_dependencies @@ -420,6 +426,7 @@ main() { echo "βœ… Updated branding text throughout app" echo "βœ… Updated AI provider settings" echo "βœ… Removed YouTube video section" + echo "βœ… Fixed ChatInput.tsx references" echo "βœ… Updated app metadata and title bar" echo "βœ… Cleaned up unused imports" echo "βœ… Installed dependencies" diff --git a/src/__tests__/chat_stream_handlers.test.ts b/src/__tests__/chat_stream_handlers.test.ts index 5ddf638..14f1abb 100644 --- a/src/__tests__/chat_stream_handlers.test.ts +++ b/src/__tests__/chat_stream_handlers.test.ts @@ -1,16 +1,16 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { - getDyadWriteTags, - getDyadRenameTags, - getDyadAddDependencyTags, - getDyadDeleteTags, + getMoreMinimoreWriteTags, + getMoreMinimoreRenameTags, + getMoreMinimoreAddDependencyTags, + getMoreMinimoreDeleteTags, } from "../ipc/utils/dyad_tag_parser"; import { processFullResponseActions } from "../ipc/processors/response_processor"; import { - removeDyadTags, - hasUnclosedDyadWrite, + removeMoreMinimoreTags, + hasUnclosedMoreMinimoreWrite, } from "../ipc/handlers/chat_stream_handlers"; import fs from "node:fs"; import { db } from "../db"; @@ -58,9 +58,9 @@ vi.mock("../ipc/utils/git_utils", () => ({ getGitUncommittedFiles: vi.fn().mockResolvedValue([]), })); -// Mock paths module to control getDyadAppPath +// Mock paths module to control getMoreMinimoreAppPath vi.mock("../paths/paths", () => ({ - getDyadAppPath: vi.fn().mockImplementation((appPath) => { + getMoreMinimoreAppPath: vi.fn().mockImplementation((appPath) => { return `/mock/user/data/path/${appPath}`; }), getUserDataPath: vi.fn().mockReturnValue("/mock/user/data/path"), @@ -85,49 +85,49 @@ vi.mock("../db", () => ({ }, })); -describe("getDyadAddDependencyTags", () => { +describe("getMoreMinimoreAddDependencyTags", () => { it("should return an empty array when no dyad-add-dependency tags are found", () => { - const result = getDyadAddDependencyTags("No dyad-add-dependency tags here"); + const result = getMoreMinimoreAddDependencyTags("No dyad-add-dependency tags here"); expect(result).toEqual([]); }); it("should return an array of dyad-add-dependency tags", () => { - const result = getDyadAddDependencyTags( + const result = getMoreMinimoreAddDependencyTags( ``, ); expect(result).toEqual(["uuid"]); }); it("should return all the packages in the dyad-add-dependency tags", () => { - const result = getDyadAddDependencyTags( + const result = getMoreMinimoreAddDependencyTags( ``, ); expect(result).toEqual(["pkg1", "pkg2"]); }); it("should return all the packages in the dyad-add-dependency tags", () => { - const result = getDyadAddDependencyTags( + const result = getMoreMinimoreAddDependencyTags( `txt beforetext after`, ); expect(result).toEqual(["pkg1", "pkg2"]); }); it("should return all the packages in multiple dyad-add-dependency tags", () => { - const result = getDyadAddDependencyTags( + const result = getMoreMinimoreAddDependencyTags( `txt beforetxt betweentext after`, ); expect(result).toEqual(["pkg1", "pkg2", "pkg3"]); }); }); -describe("getDyadWriteTags", () => { +describe("getMoreMinimoreWriteTags", () => { it("should return an empty array when no dyad-write tags are found", () => { - const result = getDyadWriteTags("No dyad-write tags here"); + const result = getMoreMinimoreWriteTags("No dyad-write tags here"); expect(result).toEqual([]); }); it("should return a dyad-write tag", () => { const result = - getDyadWriteTags(` + getMoreMinimoreWriteTags(` import React from "react"; console.log("TodoItem"); `); @@ -143,7 +143,7 @@ console.log("TodoItem");`, it("should strip out code fence (if needed) from a dyad-write tag", () => { const result = - getDyadWriteTags(` + getMoreMinimoreWriteTags(` \`\`\`tsx import React from "react"; console.log("TodoItem"); @@ -161,7 +161,7 @@ console.log("TodoItem");`, }); it("should handle missing description", () => { - const result = getDyadWriteTags(` + const result = getMoreMinimoreWriteTags(` import React from 'react'; @@ -176,7 +176,7 @@ import React from 'react'; }); it("should handle extra space", () => { - const result = getDyadWriteTags( + const result = getMoreMinimoreWriteTags( cleanFullResponse(` import React from 'react'; @@ -193,7 +193,7 @@ import React from 'react'; }); it("should handle nested tags", () => { - const result = getDyadWriteTags( + const result = getMoreMinimoreWriteTags( cleanFullResponse(` BEFORE TAG @@ -223,7 +223,7 @@ AFTER TAG const cleanedInput = cleanFullResponse(inputWithNestedTags); - const result = getDyadWriteTags(cleanedInput); + const result = getMoreMinimoreWriteTags(cleanedInput); expect(result).toEqual([ { path: "src/pages/locations/neighborhoods/louisville/Highlands.tsx", @@ -238,7 +238,7 @@ AFTER TAG // This simulates what cleanFullResponse should do const cleanedInput = cleanFullResponse(inputWithMultipleNestedTags); - const result = getDyadWriteTags(cleanedInput); + const result = getMoreMinimoreWriteTags(cleanedInput); expect(result).toEqual([ { path: "src/file.tsx", @@ -254,7 +254,7 @@ AFTER TAG // This simulates what cleanFullResponse should do const cleanedInput = cleanFullResponse(inputWithNestedInMultipleAttrs); - const result = getDyadWriteTags(cleanedInput); + const result = getMoreMinimoreWriteTags(cleanedInput); expect(result).toEqual([ { path: "src/<component>.tsx", @@ -265,7 +265,7 @@ AFTER TAG }); it("should return an array of dyad-write tags", () => { - const result = getDyadWriteTags( + const result = getMoreMinimoreWriteTags( `I'll create a simple todo list app using React, TypeScript, and shadcn/ui components. Let's get started! First, I'll create the necessary files for our todo list application: @@ -597,14 +597,14 @@ I've created a complete todo list application with the ability to add, complete, }); }); -describe("getDyadRenameTags", () => { +describe("getMoreMinimoreRenameTags", () => { it("should return an empty array when no dyad-rename tags are found", () => { - const result = getDyadRenameTags("No dyad-rename tags here"); + const result = getMoreMinimoreRenameTags("No dyad-rename tags here"); expect(result).toEqual([]); }); it("should return an array of dyad-rename tags", () => { - const result = getDyadRenameTags( + const result = getMoreMinimoreRenameTags( ` `, ); @@ -618,14 +618,14 @@ describe("getDyadRenameTags", () => { }); }); -describe("getDyadDeleteTags", () => { +describe("getMoreMinimoreDeleteTags", () => { it("should return an empty array when no dyad-delete tags are found", () => { - const result = getDyadDeleteTags("No dyad-delete tags here"); + const result = getMoreMinimoreDeleteTags("No dyad-delete tags here"); expect(result).toEqual([]); }); it("should return an array of dyad-delete paths", () => { - const result = getDyadDeleteTags( + const result = getMoreMinimoreDeleteTags( ` `, ); @@ -963,39 +963,39 @@ describe("processFullResponse", () => { }); }); -describe("removeDyadTags", () => { +describe("removeMoreMinimoreTags", () => { it("should return empty string when input is empty", () => { - const result = removeDyadTags(""); + const result = removeMoreMinimoreTags(""); expect(result).toBe(""); }); it("should return the same text when no dyad tags are present", () => { const text = "This is a regular text without any dyad tags."; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe(text); }); it("should remove a single dyad-write tag", () => { const text = `Before text console.log('hello'); After text`; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe("Before text After text"); }); it("should remove a single dyad-delete tag", () => { const text = `Before text After text`; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe("Before text After text"); }); it("should remove a single dyad-rename tag", () => { const text = `Before text After text`; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe("Before text After text"); }); it("should remove multiple different dyad tags", () => { const text = `Start code here middle end finish`; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe("Start middle end finish"); }); @@ -1011,19 +1011,19 @@ const Component = () => { export default Component; After`; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe("Before\n\nAfter"); }); it("should handle dyad tags with complex attributes", () => { const text = `Text const x = "hello world"; more text`; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe("Text more text"); }); it("should remove dyad tags and trim whitespace", () => { const text = ` code `; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe(""); }); @@ -1032,19 +1032,19 @@ After`; const html = '
Hello
'; const component = ;
`; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe(""); }); it("should handle self-closing dyad tags", () => { const text = `Before After`; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe('Before After'); }); it("should handle malformed dyad tags gracefully", () => { const text = `Before unclosed tag After`; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe('Before unclosed tag After'); }); @@ -1053,51 +1053,51 @@ const component = ; const regex = /]*>.*?/g; const special = "Special chars: @#$%^&*()[]{}|\\"; `; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe(""); }); it("should handle multiple dyad tags of the same type", () => { const text = `code1 between code2`; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe("between"); }); it("should handle dyad tags with custom tag names", () => { const text = `Before content After`; - const result = removeDyadTags(text); + const result = removeMoreMinimoreTags(text); expect(result).toBe("Before After"); }); }); -describe("hasUnclosedDyadWrite", () => { +describe("hasUnclosedMoreMinimoreWrite", () => { it("should return false when there are no dyad-write tags", () => { const text = "This is just regular text without any dyad tags."; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(false); }); it("should return false when dyad-write tag is properly closed", () => { const text = `console.log('hello');`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(false); }); it("should return true when dyad-write tag is not closed", () => { const text = `console.log('hello');`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(true); }); it("should return false when dyad-write tag with attributes is properly closed", () => { const text = `console.log('hello');`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(false); }); it("should return true when dyad-write tag with attributes is not closed", () => { const text = `console.log('hello');`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(true); }); @@ -1105,7 +1105,7 @@ describe("hasUnclosedDyadWrite", () => { const text = `code1 Some text in between code2`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(false); }); @@ -1113,7 +1113,7 @@ describe("hasUnclosedDyadWrite", () => { const text = `code1 Some text in between code2`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(true); }); @@ -1121,7 +1121,7 @@ describe("hasUnclosedDyadWrite", () => { const text = `code1 Some text in between code2`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(false); }); @@ -1139,7 +1139,7 @@ const Component = () => { export default Component; `; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(false); }); @@ -1156,7 +1156,7 @@ const Component = () => { }; export default Component;`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(true); }); @@ -1165,7 +1165,7 @@ export default Component;`; const message = "Hello 'world'"; const regex = /]*>/g; `; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(false); }); @@ -1173,7 +1173,7 @@ const regex = /]*>/g; const text = `Some text before the tag console.log('hello'); Some text after the tag`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(false); }); @@ -1181,19 +1181,19 @@ Some text after the tag`; const text = `Some text before the tag console.log('hello'); Some text after the unclosed tag`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(true); }); it("should handle empty dyad-write tags", () => { const text = ``; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(false); }); it("should handle unclosed empty dyad-write tags", () => { const text = ``; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(true); }); @@ -1201,13 +1201,13 @@ Some text after the unclosed tag`; const text = `completed content unclosed content final content`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(false); }); it("should handle tags with special characters in attributes", () => { const text = `content`; - const result = hasUnclosedDyadWrite(text); + const result = hasUnclosedMoreMinimoreWrite(text); expect(result).toBe(false); }); }); diff --git a/src/components/AppUpgrades.tsx b/src/components/AppUpgrades.tsx index 0105505..ab6892d 100644 --- a/src/components/AppUpgrades.tsx +++ b/src/components/AppUpgrades.tsx @@ -125,7 +125,7 @@ export function AppUpgrades({ appId }: { appId: number | null }) { onClick={(e) => { e.stopPropagation(); IpcClient.getInstance().openExternalUrl( - upgrade.manualUpgradeUrl ?? "https://dyad.sh/docs", + upgrade.manualUpgradeUrl ?? "https://moreminimore.com/docs", ); }} className="underline font-medium hover:dark:text-red-200" diff --git a/src/components/CapacitorControls.tsx b/src/components/CapacitorControls.tsx index 2f65675..a0c883c 100644 --- a/src/components/CapacitorControls.tsx +++ b/src/components/CapacitorControls.tsx @@ -136,7 +136,7 @@ export function CapacitorControls({ appId }: CapacitorControlsProps) { onClick={() => { // TODO: Add actual help link IpcClient.getInstance().openExternalUrl( - "https://dyad.sh/docs/guides/mobile-app#troubleshooting", + "https://moreminimore.com/docs/guides/mobile-app#troubleshooting", ); }} className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 flex items-center gap-1" diff --git a/src/components/GitHubConnector.tsx b/src/components/GitHubConnector.tsx index dd4113f..7d01896 100644 --- a/src/components/GitHubConnector.tsx +++ b/src/components/GitHubConnector.tsx @@ -204,7 +204,7 @@ function ConnectedGitHubConnector({ onClick={(e) => { e.preventDefault(); IpcClient.getInstance().openExternalUrl( - "https://www.dyad.sh/docs/integrations/github#troubleshooting", + "https://www.moreminimore.com/docs/integrations/github#troubleshooting", ); }} className="cursor-pointer text-blue-600 hover:underline dark:text-blue-400" diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index 06b2b8e..6c0797d 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -45,7 +45,7 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) { const selectedChatId = useAtomValue(selectedChatIdAtom); const { settings } = useSettings(); const { userBudget } = useUserBudgetInfo(); - const isDyadProUser = settings?.providerSettings?.["auto"]?.apiKey?.value; + const isMoreMinimoreProUser = settings?.providerSettings?.["auto"]?.apiKey?.value; // Function to reset all dialog state const resetDialogState = () => { @@ -118,7 +118,7 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"} const encodedBody = encodeURIComponent(issueBody); const encodedTitle = encodeURIComponent("[bug] "); const labels = ["bug"]; - if (isDyadProUser) { + if (isMoreMinimoreProUser) { labels.push("pro"); } const githubIssueUrl = `https://github.com/dyad-sh/dyad/issues/new?title=${encodedTitle}&labels=${labels}&body=${encodedBody}`; @@ -243,7 +243,7 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"} const encodedBody = encodeURIComponent(issueBody); const encodedTitle = encodeURIComponent("[session report] "); const labels = ["support"]; - if (isDyadProUser) { + if (isMoreMinimoreProUser) { labels.push("pro"); } const githubIssueUrl = `https://github.com/dyad-sh/dyad/issues/new?title=${encodedTitle}&labels=${labels}&body=${encodedBody}`; @@ -396,7 +396,7 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"} If you need help or want to report an issue, here are some options:
- {isDyadProUser ? ( + {isMoreMinimoreProUser ? (
); } -export function ManageDyadProButton() { +export function ManageMoreMinimoreProButton() { return (
)} {(!settings?.enableProSmartFilesContextMode || - !settings?.enableDyadPro) && ( + !settings?.enableMoreMinimorePro) && (
Optimize your tokens with{" "} - settings?.enableDyadPro + settings?.enableMoreMinimorePro ? IpcClient.getInstance().openExternalUrl( - "https://www.dyad.sh/docs/guides/ai-models/pro-modes#smart-context", + "https://www.moreminimore.com/docs/guides/ai-models/pro-modes#smart-context", ) : IpcClient.getInstance().openExternalUrl( - "https://dyad.sh/pro#ai", + "https://moreminimore.com/pro#ai", ) } className="text-blue-500 dark:text-blue-400 cursor-pointer hover:underline" diff --git a/src/components/preview_panel/AnnotatorOnlyForPro.tsx b/src/components/preview_panel/AnnotatorOnlyForPro.tsx index 9925cf4..da6e528 100644 --- a/src/components/preview_panel/AnnotatorOnlyForPro.tsx +++ b/src/components/preview_panel/AnnotatorOnlyForPro.tsx @@ -8,7 +8,7 @@ interface AnnotatorOnlyForProProps { export const AnnotatorOnlyForPro = ({ onGoBack }: AnnotatorOnlyForProProps) => { const handleGetPro = () => { - IpcClient.getInstance().openExternalUrl("https://dyad.sh/pro"); + IpcClient.getInstance().openExternalUrl("https://moreminimore.com/pro"); }; return ( diff --git a/src/components/preview_panel/PreviewIframe.tsx.bak b/src/components/preview_panel/PreviewIframe.tsx.bak deleted file mode 100644 index a2bc990..0000000 --- a/src/components/preview_panel/PreviewIframe.tsx.bak +++ /dev/null @@ -1,1081 +0,0 @@ -import { - selectedAppIdAtom, - appUrlAtom, - appOutputAtom, - previewErrorMessageAtom, -} from "@/atoms/appAtoms"; -import { useAtomValue, useSetAtom, useAtom } from "jotai"; -import { useEffect, useRef, useState } from "react"; -import { - ArrowLeft, - ArrowRight, - RefreshCw, - ExternalLink, - Loader2, - X, - Sparkles, - ChevronDown, - Lightbulb, - ChevronRight, - MousePointerClick, - Power, - MonitorSmartphone, - Monitor, - Tablet, - Smartphone, - Pen, -} from "lucide-react"; -import { selectedChatIdAtom } from "@/atoms/chatAtoms"; -import { CopyErrorMessage } from "@/components/CopyErrorMessage"; -import { IpcClient } from "@/ipc/ipc_client"; - -import { useParseRouter } from "@/hooks/useParseRouter"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { useStreamChat } from "@/hooks/useStreamChat"; -import { - selectedComponentsPreviewAtom, - visualEditingSelectedComponentAtom, - currentComponentCoordinatesAtom, - previewIframeRefAtom, - annotatorModeAtom, - screenshotDataUrlAtom, - pendingVisualChangesAtom, -} from "@/atoms/previewAtoms"; -import { ComponentSelection } from "@/ipc/ipc_types"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; -import { useRunApp } from "@/hooks/useRunApp"; -import { useShortcut } from "@/hooks/useShortcut"; -import { cn } from "@/lib/utils"; -import { normalizePath } from "../../../shared/normalizePath"; -import { showError } from "@/lib/toast"; -import { useAttachments } from "@/hooks/useAttachments"; -import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo"; -import { Annotator } from "@/pro/ui/components/Annotator/Annotator"; -import { VisualEditingToolbar } from "./VisualEditingToolbar"; - -interface ErrorBannerProps { - error: { message: string; source: "preview-app" | "dyad-app" } | undefined; - onDismiss: () => void; - onAIFix: () => void; -} - -const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => { - const [isCollapsed, setIsCollapsed] = useState(true); - const { isStreaming } = useStreamChat(); - if (!error) return null; - const isDockerError = error.message.includes("Cannot connect to the Docker"); - - const getTruncatedError = () => { - const firstLine = error.message.split("\n")[0]; - const snippetLength = 250; - const snippet = error.message.substring(0, snippetLength); - return firstLine.length < snippet.length - ? firstLine - : snippet + (snippet.length === snippetLength ? "..." : ""); - }; - - return ( -
- {/* Close button in top left */} - - - {/* Add a little chip that says "Internal error" if source is "dyad-app" */} - {error.source === "dyad-app" && ( -
- Internal MoreMinimore error -
- )} - - {/* Error message in the middle */} -
-
setIsCollapsed(!isCollapsed)} - > - - {isCollapsed ? getTruncatedError() : error.message} -
-
- - {/* Tip message */} -
-
-
- -
- - Tip: - {isDockerError - ? "Make sure Docker Desktop is running and try restarting the app." - : error.source === "dyad-app" - ? "Try restarting the MoreMinimore app or restarting your computer to see if that fixes the error." - : "Check if restarting the app fixes the error."} - -
-
- - {/* Action buttons at the bottom */} - {!isDockerError && error.source === "preview-app" && ( -
- - -
- )} -
- ); -}; - -// Preview iframe component -export const PreviewIframe = ({ loading }: { loading: boolean }) => { - const selectedAppId = useAtomValue(selectedAppIdAtom); - const { appUrl, originalUrl } = useAtomValue(appUrlAtom); - const setAppOutput = useSetAtom(appOutputAtom); - // State to trigger iframe reload - const [reloadKey, setReloadKey] = useState(0); - const [errorMessage, setErrorMessage] = useAtom(previewErrorMessageAtom); - const selectedChatId = useAtomValue(selectedChatIdAtom); - const { streamMessage } = useStreamChat(); - const { routes: availableRoutes } = useParseRouter(selectedAppId); - const { restartApp } = useRunApp(); - const { userBudget } = useUserBudgetInfo(); - const isProMode = !!userBudget; - - // Navigation state - const [isComponentSelectorInitialized, setIsComponentSelectorInitialized] = - useState(false); - const [canGoBack, setCanGoBack] = useState(false); - const [canGoForward, setCanGoForward] = useState(false); - const [navigationHistory, setNavigationHistory] = useState([]); - const [currentHistoryPosition, setCurrentHistoryPosition] = useState(0); - const setSelectedComponentsPreview = useSetAtom( - selectedComponentsPreviewAtom, - ); - const [visualEditingSelectedComponent, setVisualEditingSelectedComponent] = - useAtom(visualEditingSelectedComponentAtom); - const setCurrentComponentCoordinates = useSetAtom( - currentComponentCoordinatesAtom, - ); - const setPreviewIframeRef = useSetAtom(previewIframeRefAtom); - const iframeRef = useRef(null); - const [isPicking, setIsPicking] = useState(false); - const [annotatorMode, setAnnotatorMode] = useAtom(annotatorModeAtom); - const [screenshotDataUrl, setScreenshotDataUrl] = useAtom( - screenshotDataUrlAtom, - ); - - const { addAttachments } = useAttachments(); - const setPendingChanges = useSetAtom(pendingVisualChangesAtom); - - // AST Analysis State - const [isDynamicComponent, setIsDynamicComponent] = useState(false); - const [hasStaticText, setHasStaticText] = useState(false); - - // Device mode state - type DeviceMode = "desktop" | "tablet" | "mobile"; - const [deviceMode, setDeviceMode] = useState("desktop"); - const [isDevicePopoverOpen, setIsDevicePopoverOpen] = useState(false); - - // Device configurations - const deviceWidthConfig = { - tablet: 768, - mobile: 375, - }; - - //detect if the user is using Mac - const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0; - - const analyzeComponent = async (componentId: string) => { - if (!componentId || !selectedAppId) return; - - try { - const result = await IpcClient.getInstance().analyzeComponent({ - appId: selectedAppId, - componentId, - }); - setIsDynamicComponent(result.isDynamic); - setHasStaticText(result.hasStaticText); - - // Automatically enable text editing if component has static text - if (result.hasStaticText && iframeRef.current?.contentWindow) { - iframeRef.current.contentWindow.postMessage( - { - type: "enable-dyad-text-editing", - data: { - componentId: componentId, - runtimeId: visualEditingSelectedComponent?.runtimeId, - }, - }, - "*", - ); - } - } catch (err) { - console.error("Failed to analyze component", err); - setIsDynamicComponent(false); - setHasStaticText(false); - } - }; - - const handleTextUpdated = async (data: any) => { - const { componentId, text } = data; - if (!componentId || !selectedAppId) return; - - // Parse componentId to extract file path and line number - const [filePath, lineStr] = componentId.split(":"); - const lineNumber = parseInt(lineStr, 10); - - if (!filePath || isNaN(lineNumber)) { - console.error("Invalid componentId format:", componentId); - return; - } - - // Store text change in pending changes - setPendingChanges((prev) => { - const updated = new Map(prev); - const existing = updated.get(componentId); - - updated.set(componentId, { - componentId: componentId, - componentName: - existing?.componentName || visualEditingSelectedComponent?.name || "", - relativePath: filePath, - lineNumber: lineNumber, - styles: existing?.styles || {}, - textContent: text, - }); - - return updated; - }); - }; - - // Function to get current styles from selected element - const getCurrentElementStyles = () => { - if (!iframeRef.current?.contentWindow || !visualEditingSelectedComponent) - return; - - try { - // Send message to iframe to get current styles - iframeRef.current.contentWindow.postMessage( - { - type: "get-dyad-component-styles", - data: { - elementId: visualEditingSelectedComponent.id, - runtimeId: visualEditingSelectedComponent.runtimeId, - }, - }, - "*", - ); - } catch (error) { - console.error("Failed to get element styles:", error); - } - }; - useEffect(() => { - setAnnotatorMode(false); - }, []); - // Reset visual editing state when app changes or component unmounts - useEffect(() => { - return () => { - // Cleanup on unmount or when app changes - setVisualEditingSelectedComponent(null); - setPendingChanges(new Map()); - setCurrentComponentCoordinates(null); - }; - }, [selectedAppId]); - - // Update iframe ref atom - useEffect(() => { - setPreviewIframeRef(iframeRef.current); - }, [iframeRef.current, setPreviewIframeRef]); - - // Send pro mode status to iframe - useEffect(() => { - if (iframeRef.current?.contentWindow && isComponentSelectorInitialized) { - iframeRef.current.contentWindow.postMessage( - { type: "dyad-pro-mode", enabled: isProMode }, - "*", - ); - } - }, [isProMode, isComponentSelectorInitialized]); - - // Add message listener for iframe errors and navigation events - useEffect(() => { - const handleMessage = (event: MessageEvent) => { - // Only handle messages from our iframe - if (event.source !== iframeRef.current?.contentWindow) { - return; - } - - if (event.data?.type === "dyad-component-selector-initialized") { - setIsComponentSelectorInitialized(true); - iframeRef.current?.contentWindow?.postMessage( - { type: "dyad-pro-mode", enabled: isProMode }, - "*", - ); - return; - } - - if (event.data?.type === "dyad-text-updated") { - handleTextUpdated(event.data); - return; - } - - if (event.data?.type === "dyad-text-finalized") { - handleTextUpdated(event.data); - return; - } - - if (event.data?.type === "dyad-component-selected") { - console.log("Component picked:", event.data); - - const component = parseComponentSelection(event.data); - - if (!component) return; - - // Store the coordinates - if (event.data.coordinates && isProMode) { - setCurrentComponentCoordinates(event.data.coordinates); - } - - // Add to selected components if not already there - setSelectedComponentsPreview((prev) => { - const exists = prev.some((c) => { - // Check by runtimeId if available otherwise by id - // Stored components may have lost their runtimeId after re-renders or reloading the page - if (component.runtimeId && c.runtimeId) { - return c.runtimeId === component.runtimeId; - } - return c.id === component.id; - }); - if (exists) { - return prev; - } - return [...prev, component]; - }); - - if (isProMode) { - // Set as the highlighted component for visual editing - setVisualEditingSelectedComponent(component); - // Trigger AST analysis - analyzeComponent(component.id); - } - - return; - } - - if (event.data?.type === "dyad-component-deselected") { - const componentId = event.data.componentId; - if (componentId) { - // Disable text editing for the deselected component - if (iframeRef.current?.contentWindow) { - iframeRef.current.contentWindow.postMessage( - { - type: "disable-dyad-text-editing", - data: { componentId }, - }, - "*", - ); - } - - setSelectedComponentsPreview((prev) => - prev.filter((c) => c.id !== componentId), - ); - setVisualEditingSelectedComponent((prev) => { - const shouldClear = prev?.id === componentId; - if (shouldClear) { - setCurrentComponentCoordinates(null); - } - return shouldClear ? null : prev; - }); - } - return; - } - - if (event.data?.type === "dyad-component-coordinates-updated") { - if (event.data.coordinates) { - setCurrentComponentCoordinates(event.data.coordinates); - } - return; - } - - if (event.data?.type === "dyad-screenshot-response") { - if (event.data.success && event.data.dataUrl) { - setScreenshotDataUrl(event.data.dataUrl); - setAnnotatorMode(true); - } else { - showError(event.data.error); - } - return; - } - - const { type, payload } = event.data as { - type: - | "window-error" - | "unhandled-rejection" - | "iframe-sourcemapped-error" - | "build-error-report" - | "pushState" - | "replaceState"; - payload?: { - message?: string; - stack?: string; - reason?: string; - newUrl?: string; - file?: string; - frame?: string; - }; - }; - - if ( - type === "window-error" || - type === "unhandled-rejection" || - type === "iframe-sourcemapped-error" - ) { - const stack = - type === "iframe-sourcemapped-error" - ? payload?.stack?.split("\n").slice(0, 1).join("\n") - : payload?.stack; - const errorMessage = `Error ${ - payload?.message || payload?.reason - }\nStack trace: ${stack}`; - console.error("Iframe error:", errorMessage); - setErrorMessage({ message: errorMessage, source: "preview-app" }); - setAppOutput((prev) => [ - ...prev, - { - message: `Iframe error: ${errorMessage}`, - type: "client-error", - appId: selectedAppId!, - timestamp: Date.now(), - }, - ]); - } else if (type === "build-error-report") { - console.debug(`Build error report: ${payload}`); - const errorMessage = `${payload?.message} from file ${payload?.file}.\n\nSource code:\n${payload?.frame}`; - setErrorMessage({ message: errorMessage, source: "preview-app" }); - setAppOutput((prev) => [ - ...prev, - { - message: `Build error report: ${JSON.stringify(payload)}`, - type: "client-error", - appId: selectedAppId!, - timestamp: Date.now(), - }, - ]); - } else if (type === "pushState" || type === "replaceState") { - console.debug(`Navigation event: ${type}`, payload); - - // Update navigation history based on the type of state change - if (type === "pushState" && payload?.newUrl) { - // For pushState, we trim any forward history and add the new URL - const newHistory = [ - ...navigationHistory.slice(0, currentHistoryPosition + 1), - payload.newUrl, - ]; - setNavigationHistory(newHistory); - setCurrentHistoryPosition(newHistory.length - 1); - } else if (type === "replaceState" && payload?.newUrl) { - // For replaceState, we replace the current URL - const newHistory = [...navigationHistory]; - newHistory[currentHistoryPosition] = payload.newUrl; - setNavigationHistory(newHistory); - } - } - }; - - window.addEventListener("message", handleMessage); - return () => window.removeEventListener("message", handleMessage); - }, [ - navigationHistory, - currentHistoryPosition, - selectedAppId, - errorMessage, - setErrorMessage, - setIsComponentSelectorInitialized, - setSelectedComponentsPreview, - setVisualEditingSelectedComponent, - ]); - - useEffect(() => { - // Update navigation buttons state - setCanGoBack(currentHistoryPosition > 0); - setCanGoForward(currentHistoryPosition < navigationHistory.length - 1); - }, [navigationHistory, currentHistoryPosition]); - - // Initialize navigation history when iframe loads - useEffect(() => { - if (appUrl) { - setNavigationHistory([appUrl]); - setCurrentHistoryPosition(0); - setCanGoBack(false); - setCanGoForward(false); - } - }, [appUrl]); - - // Get current styles when component is selected for visual editing - useEffect(() => { - if (visualEditingSelectedComponent) { - getCurrentElementStyles(); - } - }, [visualEditingSelectedComponent]); - - // Function to activate component selector in the iframe - const handleActivateComponentSelector = () => { - if (iframeRef.current?.contentWindow) { - const newIsPicking = !isPicking; - if (!newIsPicking) { - // Clean up any text editing states when deactivating - iframeRef.current.contentWindow.postMessage( - { type: "cleanup-all-text-editing" }, - "*", - ); - } - setIsPicking(newIsPicking); - setVisualEditingSelectedComponent(null); - iframeRef.current.contentWindow.postMessage( - { - type: newIsPicking - ? "activate-dyad-component-selector" - : "deactivate-dyad-component-selector", - }, - "*", - ); - } - }; - - // Function to handle annotator button click - const handleAnnotatorClick = () => { - if (annotatorMode) { - setAnnotatorMode(false); - return; - } - if (iframeRef.current?.contentWindow) { - iframeRef.current.contentWindow.postMessage( - { - type: "dyad-take-screenshot", - }, - "*", - ); - } - }; - - // Activate component selector using a shortcut - useShortcut( - "c", - { shift: true, ctrl: !isMac, meta: isMac }, - handleActivateComponentSelector, - isComponentSelectorInitialized, - iframeRef, - ); - - // Function to navigate back - const handleNavigateBack = () => { - if (canGoBack && iframeRef.current?.contentWindow) { - iframeRef.current.contentWindow.postMessage( - { - type: "navigate", - payload: { direction: "backward" }, - }, - "*", - ); - - // Update our local state - setCurrentHistoryPosition((prev) => prev - 1); - setCanGoBack(currentHistoryPosition - 1 > 0); - setCanGoForward(true); - } - }; - - // Function to navigate forward - const handleNavigateForward = () => { - if (canGoForward && iframeRef.current?.contentWindow) { - iframeRef.current.contentWindow.postMessage( - { - type: "navigate", - payload: { direction: "forward" }, - }, - "*", - ); - - // Update our local state - setCurrentHistoryPosition((prev) => prev + 1); - setCanGoBack(true); - setCanGoForward( - currentHistoryPosition + 1 < navigationHistory.length - 1, - ); - } - }; - - // Function to handle reload - const handleReload = () => { - setReloadKey((prevKey) => prevKey + 1); - setErrorMessage(undefined); - // Reset visual editing state - setVisualEditingSelectedComponent(null); - setPendingChanges(new Map()); - setCurrentComponentCoordinates(null); - // Optionally, add logic here if you need to explicitly stop/start the app again - // For now, just changing the key should remount the iframe - console.debug("Reloading iframe preview for app", selectedAppId); - }; - - // Function to navigate to a specific route - const navigateToRoute = (path: string) => { - if (iframeRef.current?.contentWindow && appUrl) { - // Create the full URL by combining the base URL with the path - const baseUrl = new URL(appUrl).origin; - const newUrl = `${baseUrl}${path}`; - - // Navigate to the URL - iframeRef.current.contentWindow.location.href = newUrl; - - // iframeRef.current.src = newUrl; - - // Update navigation history - const newHistory = [ - ...navigationHistory.slice(0, currentHistoryPosition + 1), - newUrl, - ]; - setNavigationHistory(newHistory); - setCurrentHistoryPosition(newHistory.length - 1); - setCanGoBack(true); - setCanGoForward(false); - } - }; - - // Display loading state - if (loading) { - return ( -
-
-
-
-
-
-
-

- Preparing app preview... -

-
-
- ); - } - - // Display message if no app is selected - if (selectedAppId === null) { - return ( -
- Select an app to see the preview. -
- ); - } - - const onRestart = () => { - restartApp(); - }; - - return ( -
- {/* Browser-style header - hide when annotator is active */} - {!annotatorMode && ( -
- {/* Navigation Buttons */} -
- - - - - - -

- {isPicking - ? "Deactivate component selector" - : "Select component"} -

-

{isMac ? "⌘ + ⇧ + C" : "Ctrl + ⇧ + C"}

-
-
-
- - - - - - -

- {annotatorMode - ? "Annotator mode active" - : "Activate annotator"} -

-
-
-
- - - -
- - {/* Address Bar with Routes Dropdown - using shadcn/ui dropdown-menu */} -
- - -
- - {navigationHistory[currentHistoryPosition] - ? new URL(navigationHistory[currentHistoryPosition]) - .pathname - : "/"} - - -
-
- - {availableRoutes.length > 0 ? ( - availableRoutes.map((route) => ( - navigateToRoute(route.path)} - className="flex justify-between" - > - {route.label} - - {route.path} - - - )) - ) : ( - - Loading routes... - - )} - -
-
- - {/* Action Buttons */} -
- - - - {/* Device Mode Button */} - - - - - e.preventDefault()} - onInteractOutside={(e) => e.preventDefault()} - > - - { - if (value) { - setDeviceMode(value as DeviceMode); - setIsDevicePopoverOpen(false); - } - }} - variant="outline" - > - {/* Tooltips placed inside items instead of wrapping - to avoid asChild prop merging that breaks highlighting */} - - - - - - - - -

Desktop

-
-
-
- - - - - - - - -

Tablet

-
-
-
- - - - - - - - -

Mobile

-
-
-
-
-
-
-
-
-
- )} - -
- setErrorMessage(undefined)} - onAIFix={() => { - if (selectedChatId) { - streamMessage({ - prompt: `Fix error: ${errorMessage?.message}`, - chatId: selectedChatId, - }); - } - }} - /> - - {!appUrl ? ( -
- -

- Starting your app server... -

-
- ) : ( -
- {annotatorMode && screenshotDataUrl ? ( -
- -
- ) : ( - <> -