提交 d2955458 authored 作者: Vittorio's avatar Vittorio

去掉广告,修改一些界面展示为中文

上级 30300641
{
"i18n-ally.localesPaths": [
"src/i18n",
"src/i18n/locales"
]
}
\ No newline at end of file
...@@ -125,7 +125,7 @@ ...@@ -125,7 +125,7 @@
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"drizzle-kit": "^0.30.6", "drizzle-kit": "^0.30.6",
"electron": "40.0.0", "electron": "^40.0.0",
"eslint": "^8.57.1", "eslint": "^8.57.1",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"happy-dom": "^17.4.4", "happy-dom": "^17.4.4",
...@@ -11534,7 +11534,7 @@ ...@@ -11534,7 +11534,7 @@
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "40.0.0", "version": "40.0.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-40.0.0.tgz", "resolved": "https://registry.npmmirror.com/electron/-/electron-40.0.0.tgz",
"integrity": "sha512-UyBy5yJ0/wm4gNugCtNPjvddjAknMTuXR2aCHioXicH7aKRKGDBPp4xqTEi/doVcB3R+MN3wfU9o8d/9pwgK2A==", "integrity": "sha512-UyBy5yJ0/wm4gNugCtNPjvddjAknMTuXR2aCHioXicH7aKRKGDBPp4xqTEi/doVcB3R+MN3wfU9o8d/9pwgK2A==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
......
...@@ -166,7 +166,7 @@ ...@@ -166,7 +166,7 @@
"babel-plugin-react-compiler": "^1.0.0", "babel-plugin-react-compiler": "^1.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"drizzle-kit": "^0.30.6", "drizzle-kit": "^0.30.6",
"electron": "40.0.0", "electron": "^40.0.0",
"eslint": "^8.57.1", "eslint": "^8.57.1",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"happy-dom": "^17.4.4", "happy-dom": "^17.4.4",
......
...@@ -76,6 +76,7 @@ describe("readSettings", () => { ...@@ -76,6 +76,7 @@ describe("readSettings", () => {
"experiments": {}, "experiments": {},
"hasRunBefore": false, "hasRunBefore": false,
"isRunning": false, "isRunning": false,
"language": "zh-CN",
"lastKnownPerformance": undefined, "lastKnownPerformance": undefined,
"previewIdleTimeoutPolicy": "default", "previewIdleTimeoutPolicy": "default",
"providerSettings": {}, "providerSettings": {},
...@@ -469,6 +470,7 @@ describe("readSettings", () => { ...@@ -469,6 +470,7 @@ describe("readSettings", () => {
"experiments": {}, "experiments": {},
"hasRunBefore": false, "hasRunBefore": false,
"isRunning": false, "isRunning": false,
"language": "zh-CN",
"lastKnownPerformance": undefined, "lastKnownPerformance": undefined,
"previewIdleTimeoutPolicy": "default", "previewIdleTimeoutPolicy": "default",
"providerSettings": {}, "providerSettings": {},
......
...@@ -13,7 +13,6 @@ import { ipc } from "@/ipc/types"; ...@@ -13,7 +13,6 @@ import { ipc } from "@/ipc/types";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { toast } from "sonner"; import { toast } from "sonner";
import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo"; import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo";
import { AiAccessBanner } from "./ProBanner";
import type { import type {
ThemeGenerationMode, ThemeGenerationMode,
ThemeGenerationModel, ThemeGenerationModel,
...@@ -330,7 +329,6 @@ export function AIGeneratorTab({ ...@@ -330,7 +329,6 @@ export function AIGeneratorTab({
Pro-only feature Pro-only feature
</p> </p>
</div> </div>
<AiAccessBanner />
</div> </div>
); );
} }
......
...@@ -14,7 +14,10 @@ import { useOpenApp } from "@/hooks/useOpenApp"; ...@@ -14,7 +14,10 @@ import { useOpenApp } from "@/hooks/useOpenApp";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { AppSearchDialog } from "./AppSearchDialog"; import { AppSearchDialog } from "./AppSearchDialog";
import { AppItem } from "./appItem"; import { AppItem } from "./appItem";
import { useTranslation } from "react-i18next";
export function AppList({ show }: { show?: boolean }) { export function AppList({ show }: { show?: boolean }) {
const { t } = useTranslation("home");
const navigate = useNavigate(); const navigate = useNavigate();
const selectedAppId = useAtomValue(selectedAppIdAtom); const selectedAppId = useAtomValue(selectedAppIdAtom);
const openApp = useOpenApp(); const openApp = useOpenApp();
...@@ -64,7 +67,7 @@ export function AppList({ show }: { show?: boolean }) { ...@@ -64,7 +67,7 @@ export function AppList({ show }: { show?: boolean }) {
className="overflow-y-auto h-[calc(100vh-112px)]" className="overflow-y-auto h-[calc(100vh-112px)]"
data-testid="app-list-container" data-testid="app-list-container"
> >
<SidebarGroupLabel>Your Apps</SidebarGroupLabel> <SidebarGroupLabel>{t("appList.title")}</SidebarGroupLabel>
<SidebarGroupContent> <SidebarGroupContent>
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<Button <Button
...@@ -73,7 +76,7 @@ export function AppList({ show }: { show?: boolean }) { ...@@ -73,7 +76,7 @@ export function AppList({ show }: { show?: boolean }) {
className="flex items-center justify-start gap-2 mx-2 py-2" className="flex items-center justify-start gap-2 mx-2 py-2"
> >
<PlusCircle size={16} /> <PlusCircle size={16} />
<span>New App</span> <span>{t("appList.newApp")}</span>
</Button> </Button>
<Button <Button
onClick={() => setIsSearchDialogOpen(!isSearchDialogOpen)} onClick={() => setIsSearchDialogOpen(!isSearchDialogOpen)}
...@@ -82,27 +85,27 @@ export function AppList({ show }: { show?: boolean }) { ...@@ -82,27 +85,27 @@ export function AppList({ show }: { show?: boolean }) {
data-testid="search-apps-button" data-testid="search-apps-button"
> >
<Search size={16} /> <Search size={16} />
<span>Search Apps</span> <span>{t("appList.searchApps")}</span>
</Button> </Button>
{loading ? ( {loading ? (
<div className="py-2 px-4 text-sm text-gray-500"> <div className="py-2 px-4 text-sm text-gray-500">
Loading apps... {t("appList.loading")}
</div> </div>
) : error ? ( ) : error ? (
<div className="py-2 px-4 text-sm text-red-500"> <div className="py-2 px-4 text-sm text-red-500">
Error loading apps {t("appList.error")}
</div> </div>
) : apps.length === 0 ? ( ) : apps.length === 0 ? (
<div className="py-2 px-4 text-sm text-gray-500"> <div className="py-2 px-4 text-sm text-gray-500">
No apps found {t("appList.empty")}
</div> </div>
) : ( ) : (
<SidebarMenu className="space-y-1" data-testid="app-list"> <SidebarMenu className="space-y-1" data-testid="app-list">
<SidebarGroupLabel>Favorite apps</SidebarGroupLabel> <SidebarGroupLabel>{t("appList.favorites")}</SidebarGroupLabel>
{favoriteApps.length === 0 ? ( {favoriteApps.length === 0 ? (
<div className="px-4 text-xs text-gray-500 italic"> <div className="px-4 text-xs text-gray-500 italic">
Star an app from its details page to pin it here {t("appList.noFavorites")}
</div> </div>
) : ( ) : (
favoriteApps.map((app) => ( favoriteApps.map((app) => (
...@@ -114,7 +117,7 @@ export function AppList({ show }: { show?: boolean }) { ...@@ -114,7 +117,7 @@ export function AppList({ show }: { show?: boolean }) {
/> />
)) ))
)} )}
<SidebarGroupLabel>Other apps</SidebarGroupLabel> <SidebarGroupLabel>{t("appList.others")}</SidebarGroupLabel>
{nonFavoriteApps.map((app) => ( {nonFavoriteApps.map((app) => (
<AppItem <AppItem
key={app.id} key={app.id}
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useSearchApps } from "@/hooks/useSearchApps"; import { useSearchApps } from "@/hooks/useSearchApps";
import type { AppSearchResult } from "@/lib/schemas"; import type { AppSearchResult } from "@/lib/schemas";
import { useTranslation } from "react-i18next";
type AppSearchDialogProps = { type AppSearchDialogProps = {
open: boolean; open: boolean;
...@@ -25,6 +26,7 @@ export function AppSearchDialog({ ...@@ -25,6 +26,7 @@ export function AppSearchDialog({
allApps, allApps,
disableShortcut, disableShortcut,
}: AppSearchDialogProps) { }: AppSearchDialogProps) {
const { t } = useTranslation("home");
const [searchQuery, setSearchQuery] = useState<string>(""); const [searchQuery, setSearchQuery] = useState<string>("");
function useDebouncedValue<T>(value: T, delay: number): T { function useDebouncedValue<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState<T>(value); const [debounced, setDebounced] = useState<T>(value);
...@@ -108,16 +110,16 @@ export function AppSearchDialog({ ...@@ -108,16 +110,16 @@ export function AppSearchDialog({
filter={commandFilter} filter={commandFilter}
> >
<CommandInput <CommandInput
placeholder="Search apps" placeholder={t("apps.searchDialogPlaceholder")}
value={searchQuery} value={searchQuery}
onValueChange={setSearchQuery} onValueChange={setSearchQuery}
data-testid="app-search-input" data-testid="app-search-input"
/> />
<CommandList data-testid="app-search-list"> <CommandList data-testid="app-search-list">
<CommandEmpty data-testid="app-search-empty"> <CommandEmpty data-testid="app-search-empty">
No results found. {t("apps.noResults")}
</CommandEmpty> </CommandEmpty>
<CommandGroup heading="Apps" data-testid="app-search-group"> <CommandGroup heading={t("apps.title")} data-testid="app-search-group">
{appsToShow.map((app) => { {appsToShow.map((app) => {
const isSearch = searchQuery.trim() !== ""; const isSearch = searchQuery.trim() !== "";
let snippet = null; let snippet = null;
......
...@@ -22,7 +22,6 @@ import { Label } from "@/components/ui/label"; ...@@ -22,7 +22,6 @@ import { Label } from "@/components/ui/label";
import { useLoadApps } from "@/hooks/useLoadApps"; import { useLoadApps } from "@/hooks/useLoadApps";
import { useGenerateImage } from "@/hooks/useGenerateImage"; import { useGenerateImage } from "@/hooks/useGenerateImage";
import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo"; import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo";
import { AiAccessBanner } from "./ProBanner";
import { AppSearchSelect } from "./AppSearchSelect"; import { AppSearchSelect } from "./AppSearchSelect";
import type { ImageThemeMode } from "@/ipc/types"; import type { ImageThemeMode } from "@/ipc/types";
...@@ -150,7 +149,6 @@ export function ImageGeneratorDialog({ ...@@ -150,7 +149,6 @@ export function ImageGeneratorDialog({
Pro-only feature Pro-only feature
</p> </p>
</div> </div>
<AiAccessBanner />
</div> </div>
) : ( ) : (
<> <>
......
...@@ -3,8 +3,10 @@ import { Upload } from "lucide-react"; ...@@ -3,8 +3,10 @@ import { Upload } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { ImportAppDialog } from "./ImportAppDialog"; import { ImportAppDialog } from "./ImportAppDialog";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
export function ImportAppButton({ className }: { className?: string }) { export function ImportAppButton({ className }: { className?: string }) {
const { t } = useTranslation("home");
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
return ( return (
...@@ -16,7 +18,7 @@ export function ImportAppButton({ className }: { className?: string }) { ...@@ -16,7 +18,7 @@ export function ImportAppButton({ className }: { className?: string }) {
onClick={() => setIsDialogOpen(true)} onClick={() => setIsDialogOpen(true)}
> >
<Upload className="mr-2 h-4 w-4" /> <Upload className="mr-2 h-4 w-4" />
Import App {t("importApp")}
</Button> </Button>
</div> </div>
<ImportAppDialog <ImportAppDialog
......
...@@ -397,7 +397,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -397,7 +397,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Checkbox <Checkbox
id="copy-to-dyad-apps" id="copy-to-dyad-apps"
aria-label="Copy to the dyad-apps folder" aria-label={t("home:copyToDyadApps")}
checked={copyToDyadApps} checked={copyToDyadApps}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
setCopyToDyadApps(checked === true) setCopyToDyadApps(checked === true)
...@@ -452,7 +452,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -452,7 +452,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
onChange={(e) => onChange={(e) =>
setInstallCommand(e.target.value) setInstallCommand(e.target.value)
} }
placeholder="pnpm install" placeholder={t("home:installCommandPlaceholder")}
className="text-sm" className="text-sm"
disabled={importAppMutation.isPending} disabled={importAppMutation.isPending}
/> />
...@@ -464,7 +464,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -464,7 +464,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<Input <Input
value={startCommand} value={startCommand}
onChange={(e) => setStartCommand(e.target.value)} onChange={(e) => setStartCommand(e.target.value)}
placeholder="pnpm dev" placeholder={t("home:startCommandPlaceholder")}
className="text-sm" className="text-sm"
disabled={importAppMutation.isPending} disabled={importAppMutation.isPending}
/> />
...@@ -481,7 +481,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -481,7 +481,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
{hasAiRules === false && ( {hasAiRules === false && (
<Alert className="border-yellow-500/20 text-yellow-500 flex items-start gap-2"> <Alert className="border-yellow-500/20 text-yellow-500 flex items-start gap-2">
<span <span
title="AI_RULES.md lets Dyad know which tech stack to use for editing the app" title={t("home:aiRulesTooltip")}
className="flex-shrink-0 mt-1" className="flex-shrink-0 mt-1"
> >
<Info className="h-4 w-4" /> <Info className="h-4 w-4" />
...@@ -623,7 +623,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -623,7 +623,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
onChange={(e) => onChange={(e) =>
setInstallCommand(e.target.value) setInstallCommand(e.target.value)
} }
placeholder="pnpm install" placeholder={t("home:installCommandPlaceholder")}
className="text-sm" className="text-sm"
disabled={importing} disabled={importing}
/> />
...@@ -637,7 +637,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -637,7 +637,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
onChange={(e) => onChange={(e) =>
setStartCommand(e.target.value) setStartCommand(e.target.value)
} }
placeholder="pnpm dev" placeholder={t("home:startCommandPlaceholder")}
className="text-sm" className="text-sm"
disabled={importing} disabled={importing}
/> />
...@@ -705,7 +705,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -705,7 +705,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<Input <Input
value={installCommand} value={installCommand}
onChange={(e) => setInstallCommand(e.target.value)} onChange={(e) => setInstallCommand(e.target.value)}
placeholder="pnpm install" placeholder={t("home:installCommandPlaceholder")}
className="text-sm" className="text-sm"
disabled={importing} disabled={importing}
/> />
...@@ -717,7 +717,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) { ...@@ -717,7 +717,7 @@ export function ImportAppDialog({ isOpen, onClose }: ImportAppDialogProps) {
<Input <Input
value={startCommand} value={startCommand}
onChange={(e) => setStartCommand(e.target.value)} onChange={(e) => setStartCommand(e.target.value)}
placeholder="pnpm dev" placeholder={t("home:startCommandPlaceholder")}
className="text-sm" className="text-sm"
disabled={importing} disabled={importing}
/> />
......
...@@ -11,7 +11,7 @@ import { ...@@ -11,7 +11,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
const DEFAULT_LANGUAGE: Language = "en"; const DEFAULT_LANGUAGE: Language = "zh-CN";
/** /**
* Language labels shown in their native script so users can always * Language labels shown in their native script so users can always
......
...@@ -29,15 +29,11 @@ import { usePostHog } from "posthog-js/react"; ...@@ -29,15 +29,11 @@ import { usePostHog } from "posthog-js/react";
import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders"; import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders";
import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo"; import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo";
// @ts-ignore // @ts-ignore
import logo from "../../assets/logo.svg";
// @ts-ignore
import googleIcon from "../../assets/ai-logos/google-g-icon.svg"; import googleIcon from "../../assets/ai-logos/google-g-icon.svg";
// @ts-ignore // @ts-ignore
import openrouterLogo from "../../assets/ai-logos/openrouter-logo.png"; import openrouterLogo from "../../assets/ai-logos/openrouter-logo.png";
import { OnboardingBanner } from "./home/OnboardingBanner";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { DyadProTrialDialog } from "./DyadProTrialDialog";
type NodeInstallStep = type NodeInstallStep =
| "install" | "install"
...@@ -46,10 +42,9 @@ type NodeInstallStep = ...@@ -46,10 +42,9 @@ type NodeInstallStep =
| "finished-checking"; | "finished-checking";
export function SetupBanner() { export function SetupBanner() {
const { t } = useTranslation("home"); const { t } = useTranslation(["home", "common"]);
const posthog = usePostHog(); const posthog = usePostHog();
const navigate = useNavigate(); const navigate = useNavigate();
const [isOnboardingVisible, setIsOnboardingVisible] = useState(true);
const { isAnyProviderSetup, isLoading: loading } = const { isAnyProviderSetup, isLoading: loading } =
useLanguageModelProviders(); useLanguageModelProviders();
const [nodeSystemInfo, setNodeSystemInfo] = useState<NodeSystemInfo | null>( const [nodeSystemInfo, setNodeSystemInfo] = useState<NodeSystemInfo | null>(
...@@ -71,7 +66,6 @@ export function SetupBanner() { ...@@ -71,7 +66,6 @@ export function SetupBanner() {
}, [setNodeSystemInfo, setNodeCheckError]); }, [setNodeSystemInfo, setNodeCheckError]);
const [showManualConfig, setShowManualConfig] = useState(false); const [showManualConfig, setShowManualConfig] = useState(false);
const [isSelectingPath, setIsSelectingPath] = useState(false); const [isSelectingPath, setIsSelectingPath] = useState(false);
const [showDyadProTrialDialog, setShowDyadProTrialDialog] = useState(false);
const { updateSettings } = useSettings(); const { updateSettings } = useSettings();
// Add handler for manual path selection // Add handler for manual path selection
...@@ -87,15 +81,21 @@ export function SetupBanner() { ...@@ -87,15 +81,21 @@ export function SetupBanner() {
setShowManualConfig(false); setShowManualConfig(false);
} else if (result.path === null && result.canceled === false) { } else if (result.path === null && result.canceled === false) {
showError( showError(
`Could not find Node.js at the path "${result.selectedPath}"`, t("home:setup.couldNotFindNodeAtPath", {
path: result.selectedPath,
}),
); );
} }
} catch (error) { } catch (error) {
showError("Error setting Node.js path:" + error); showError(
t("home:setup.errorSettingNodePath", {
error: String(error),
}),
);
} finally { } finally {
setIsSelectingPath(false); setIsSelectingPath(false);
} }
}, [checkNode]); }, [checkNode, t, updateSettings]);
useEffect(() => { useEffect(() => {
checkNode(); checkNode();
...@@ -121,10 +121,6 @@ export function SetupBanner() { ...@@ -121,10 +121,6 @@ export function SetupBanner() {
params: { provider: "openrouter" }, params: { provider: "openrouter" },
}); });
}; };
const handleDyadProSetupClick = () => {
posthog.capture("setup-flow:ai-provider-setup:dyad:click");
setShowDyadProTrialDialog(true);
};
const handleOtherProvidersClick = () => { const handleOtherProvidersClick = () => {
posthog.capture("setup-flow:ai-provider-setup:other:click"); posthog.capture("setup-flow:ai-provider-setup:other:click");
...@@ -185,10 +181,6 @@ export function SetupBanner() { ...@@ -185,10 +181,6 @@ export function SetupBanner() {
<p className="text-xl font-medium text-zinc-700 dark:text-zinc-300 p-4 pt-6"> <p className="text-xl font-medium text-zinc-700 dark:text-zinc-300 p-4 pt-6">
{t("setup.setupDyad")} {t("setup.setupDyad")}
</p> </p>
<OnboardingBanner
isVisible={isOnboardingVisible}
setIsVisible={setIsOnboardingVisible}
/>
<div className={bannerClasses}> <div className={bannerClasses}>
<Accordion multiple className="w-full" defaultValue={itemsNeedAction}> <Accordion multiple className="w-full" defaultValue={itemsNeedAction}>
<AccordionItem <AccordionItem
...@@ -314,27 +306,12 @@ export function SetupBanner() { ...@@ -314,27 +306,12 @@ export function SetupBanner() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{getStatusIcon(isAnyProviderSetup())} {getStatusIcon(isAnyProviderSetup())}
<span className="font-medium text-sm"> <span className="font-medium text-sm">
2. Setup AI Access {t("home:setup.setupAiAccess")}
</span> </span>
</div> </div>
</div> </div>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="px-4 pt-2 pb-4 bg-white dark:bg-zinc-900 border-t border-inherit"> <AccordionContent className="px-4 pt-2 pb-4 bg-white dark:bg-zinc-900 border-t border-inherit">
<p className="text-[15px] mb-3">
Not sure what to do? Watch the Get Started video above ☝️
</p>
<SetupProviderCard
variant="dyad"
onClick={handleDyadProSetupClick}
tabIndex={isNodeSetupComplete ? 0 : -1}
leadingIcon={
<img src={logo} alt="Dyad Logo" className="w-6 h-6 mr-0.5" />
}
title="Start with Dyad Pro free trial"
subtitle="Unlock the full power of Dyad"
chip={<>Recommended</>}
/>
<div className="mt-2 flex gap-2"> <div className="mt-2 flex gap-2">
<SetupProviderCard <SetupProviderCard
className="flex-1" className="flex-1"
...@@ -344,8 +321,8 @@ export function SetupBanner() { ...@@ -344,8 +321,8 @@ export function SetupBanner() {
leadingIcon={ leadingIcon={
<img src={googleIcon} alt="Google" className="w-4 h-4" /> <img src={googleIcon} alt="Google" className="w-4 h-4" />
} }
title="Setup Google Gemini API Key" title={t("home:setup.setupGeminiApiKey")}
chip={<>Free</>} chip={<>{t("common:free")}</>}
/> />
<SetupProviderCard <SetupProviderCard
...@@ -360,8 +337,8 @@ export function SetupBanner() { ...@@ -360,8 +337,8 @@ export function SetupBanner() {
className="w-4 h-4" className="w-4 h-4"
/> />
} }
title="Setup OpenRouter API Key" title={t("home:setup.setupOpenRouterApiKey")}
chip={<>Free</>} chip={<>{t("common:free")}</>}
/> />
</div> </div>
...@@ -378,10 +355,10 @@ export function SetupBanner() { ...@@ -378,10 +355,10 @@ export function SetupBanner() {
</div> </div>
<div> <div>
<h4 className="font-medium text-[15px] text-gray-800 dark:text-gray-300"> <h4 className="font-medium text-[15px] text-gray-800 dark:text-gray-300">
Setup other AI providers {t("home:setup.setupOtherProviders")}
</h4> </h4>
<p className="text-xs text-gray-600 dark:text-gray-400"> <p className="text-xs text-gray-600 dark:text-gray-400">
OpenAI, Anthropic and more {t("home:setup.openAiAnthropicMore")}
</p> </p>
</div> </div>
</div> </div>
...@@ -392,34 +369,27 @@ export function SetupBanner() { ...@@ -392,34 +369,27 @@ export function SetupBanner() {
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</div> </div>
<DyadProTrialDialog
isOpen={showDyadProTrialDialog}
onClose={() => setShowDyadProTrialDialog(false)}
/>
</> </>
); );
} }
function NodeJsHelpCallout() { function NodeJsHelpCallout() {
const { t } = useTranslation("home");
return ( return (
<div className="mt-3 p-3 bg-(--background-lighter) border rounded-lg text-sm"> <div className="mt-3 p-3 bg-(--background-lighter) border rounded-lg text-sm">
<p> <p>
If you run into issues, read our{" "} {t("setup.ifYouRunIntoIssues")}{" "}
<a <a
onClick={() => { onClick={() => {
ipc.system.openExternalUrl("https://www.dyad.sh/docs/help/nodejs"); ipc.system.openExternalUrl("https://www.dyad.sh/docs/help/nodejs");
}} }}
className="text-blue-600 dark:text-blue-400 hover:underline font-medium" className="text-blue-600 dark:text-blue-400 hover:underline font-medium"
> >
Node.js troubleshooting guide {t("setup.nodeTroubleshooting")}
</a> </a>
.{" "} .{" "}
</p> </p>
<p className="mt-2"> <p className="mt-2">{t("setup.stillStuck")}</p>
Still stuck? Click the <b>Help</b> button in the bottom-left corner and
then <b>Report a Bug</b>.
</p>
</div> </div>
); );
} }
...@@ -433,11 +403,12 @@ function NodeInstallButton({ ...@@ -433,11 +403,12 @@ function NodeInstallButton({
handleNodeInstallClick: () => void; handleNodeInstallClick: () => void;
finishNodeInstall: () => void; finishNodeInstall: () => void;
}) { }) {
const { t } = useTranslation("home");
switch (nodeInstallStep) { switch (nodeInstallStep) {
case "install": case "install":
return ( return (
<Button className="mt-3" onClick={handleNodeInstallClick}> <Button className="mt-3" onClick={handleNodeInstallClick}>
Install Node.js Runtime {t("setup.installNodeRuntime")}
</Button> </Button>
); );
case "continue-processing": case "continue-processing":
...@@ -445,22 +416,20 @@ function NodeInstallButton({ ...@@ -445,22 +416,20 @@ function NodeInstallButton({
<Button className="mt-3" onClick={finishNodeInstall} disabled> <Button className="mt-3" onClick={finishNodeInstall} disabled>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
Checking Node.js setup... {t("setup.checkingNodeSetup")}
</div> </div>
</Button> </Button>
); );
case "waiting-for-continue": case "waiting-for-continue":
return ( return (
<Button className="mt-3" onClick={finishNodeInstall}> <Button className="mt-3" onClick={finishNodeInstall}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">{t("setup.continueInstalled")}</div>
Continue | I installed Node.js
</div>
</Button> </Button>
); );
case "finished-checking": case "finished-checking":
return ( return (
<div className="mt-3 text-sm text-red-600 dark:text-red-400"> <div className="mt-3 text-sm text-red-600 dark:text-red-400">
Node.js not detected. Closing and re-opening Dyad usually fixes this. {t("setup.nodeNotDetected")}
</div> </div>
); );
default: default:
...@@ -473,6 +442,7 @@ export const OpenRouterSetupBanner = ({ ...@@ -473,6 +442,7 @@ export const OpenRouterSetupBanner = ({
}: { }: {
className?: string; className?: string;
}) => { }) => {
const { t } = useTranslation(["home", "common"]);
const posthog = usePostHog(); const posthog = usePostHog();
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
...@@ -490,11 +460,11 @@ export const OpenRouterSetupBanner = ({ ...@@ -490,11 +460,11 @@ export const OpenRouterSetupBanner = ({
leadingIcon={ leadingIcon={
<img src={openrouterLogo} alt="OpenRouter" className="w-4 h-4" /> <img src={openrouterLogo} alt="OpenRouter" className="w-4 h-4" />
} }
title="Setup OpenRouter API Key" title={t("home:setup.setupOpenRouterApiKey")}
chip={ chip={
<> <>
<GiftIcon className="w-3 h-3" /> <GiftIcon className="w-3 h-3" />
Free models available {t("home:setup.freeModelsAvailable")}
</> </>
} }
/> />
......
...@@ -3,8 +3,6 @@ import { ...@@ -3,8 +3,6 @@ import {
Inbox, Inbox,
Settings, Settings,
HelpCircle, HelpCircle,
Store,
BookOpen,
} from "lucide-react"; } from "lucide-react";
import { Link, useRouterState } from "@tanstack/react-router"; import { Link, useRouterState } from "@tanstack/react-router";
import { useSidebar } from "@/components/ui/sidebar"; // import useSidebar hook import { useSidebar } from "@/components/ui/sidebar"; // import useSidebar hook
...@@ -28,35 +26,25 @@ import { ChatList } from "./ChatList"; ...@@ -28,35 +26,25 @@ import { ChatList } from "./ChatList";
import { AppList } from "./AppList"; import { AppList } from "./AppList";
import { HelpDialog } from "./HelpDialog"; // Import the new dialog import { HelpDialog } from "./HelpDialog"; // Import the new dialog
import { SettingsList } from "./SettingsList"; import { SettingsList } from "./SettingsList";
import { LibraryList } from "./LibraryList"; import { useTranslation } from "react-i18next";
// Menu items. // Menu items.
const items = [ const items = [
{ {
title: "Apps", key: "apps",
to: "/", to: "/",
icon: Home, icon: Home,
}, },
{ {
title: "Chat", key: "chat",
to: "/chat", to: "/chat",
icon: Inbox, icon: Inbox,
}, },
{ {
title: "Settings", key: "settings",
to: "/settings", to: "/settings",
icon: Settings, icon: Settings,
}, },
{
title: "Library",
to: "/library",
icon: BookOpen,
},
{
title: "Hub",
to: "/hub",
icon: Store,
},
]; ];
// Hover state types // Hover state types
...@@ -64,11 +52,11 @@ type HoverState = ...@@ -64,11 +52,11 @@ type HoverState =
| "start-hover:app" | "start-hover:app"
| "start-hover:chat" | "start-hover:chat"
| "start-hover:settings" | "start-hover:settings"
| "start-hover:library"
| "clear-hover" | "clear-hover"
| "no-hover"; | "no-hover";
export function AppSidebar() { export function AppSidebar() {
const { t } = useTranslation(["home", "chat", "settings"]);
const { state, toggleSidebar } = useSidebar(); // retrieve current sidebar state const { state, toggleSidebar } = useSidebar(); // retrieve current sidebar state
const [hoverState, setHoverState] = useState<HoverState>("no-hover"); const [hoverState, setHoverState] = useState<HoverState>("no-hover");
const expandedByHover = useRef(false); const expandedByHover = useRef(false);
...@@ -98,26 +86,21 @@ export function AppSidebar() { ...@@ -98,26 +86,21 @@ export function AppSidebar() {
routerState.location.pathname.startsWith("/app-details"); routerState.location.pathname.startsWith("/app-details");
const isChatRoute = routerState.location.pathname === "/chat"; const isChatRoute = routerState.location.pathname === "/chat";
const isSettingsRoute = routerState.location.pathname.startsWith("/settings"); const isSettingsRoute = routerState.location.pathname.startsWith("/settings");
const isLibraryRoute = routerState.location.pathname.startsWith("/library");
let selectedItem: string | null = null; let selectedItem: string | null = null;
if (hoverState === "start-hover:app") { if (hoverState === "start-hover:app") {
selectedItem = "Apps"; selectedItem = "apps";
} else if (hoverState === "start-hover:chat") { } else if (hoverState === "start-hover:chat") {
selectedItem = "Chat"; selectedItem = "chat";
} else if (hoverState === "start-hover:settings") { } else if (hoverState === "start-hover:settings") {
selectedItem = "Settings"; selectedItem = "settings";
} else if (hoverState === "start-hover:library") {
selectedItem = "Library";
} else if (state === "expanded") { } else if (state === "expanded") {
if (isAppRoute) { if (isAppRoute) {
selectedItem = "Apps"; selectedItem = "apps";
} else if (isChatRoute) { } else if (isChatRoute) {
selectedItem = "Chat"; selectedItem = "chat";
} else if (isSettingsRoute) { } else if (isSettingsRoute) {
selectedItem = "Settings"; selectedItem = "settings";
} else if (isLibraryRoute) {
selectedItem = "Library";
} }
} }
...@@ -143,10 +126,9 @@ export function AppSidebar() { ...@@ -143,10 +126,9 @@ export function AppSidebar() {
</div> </div>
{/* Right Column: Chat List Section */} {/* Right Column: Chat List Section */}
<div className="w-[272px]"> <div className="w-[272px]">
<AppList show={selectedItem === "Apps"} /> <AppList show={selectedItem === "apps"} />
<ChatList show={selectedItem === "Chat"} /> <ChatList show={selectedItem === "chat"} />
<SettingsList show={selectedItem === "Settings"} /> <SettingsList show={selectedItem === "settings"} />
<LibraryList show={selectedItem === "Library"} />
</div> </div>
</div> </div>
</SidebarContent> </SidebarContent>
...@@ -161,7 +143,7 @@ export function AppSidebar() { ...@@ -161,7 +143,7 @@ export function AppSidebar() {
onClick={() => setIsHelpDialogOpen(true)} // Open dialog on click onClick={() => setIsHelpDialogOpen(true)} // Open dialog on click
> >
<HelpCircle className="h-5 w-5" /> <HelpCircle className="h-5 w-5" />
<span className={"text-xs"}>Help</span> <span className={"text-xs"}>{t("home:sidebar.help")}</span>
</SidebarMenuButton> </SidebarMenuButton>
<HelpDialog <HelpDialog
isOpen={isHelpDialogOpen} isOpen={isHelpDialogOpen}
...@@ -181,6 +163,7 @@ function AppIcons({ ...@@ -181,6 +163,7 @@ function AppIcons({
}: { }: {
onHoverChange: (state: HoverState) => void; onHoverChange: (state: HoverState) => void;
}) { }) {
const { t } = useTranslation(["home", "chat", "settings"]);
const routerState = useRouterState(); const routerState = useRouterState();
const pathname = routerState.location.pathname; const pathname = routerState.location.pathname;
...@@ -196,8 +179,15 @@ function AppIcons({ ...@@ -196,8 +179,15 @@ function AppIcons({
(item.to === "/" && pathname === "/") || (item.to === "/" && pathname === "/") ||
(item.to !== "/" && pathname.startsWith(item.to)); (item.to !== "/" && pathname.startsWith(item.to));
const label =
item.key === "apps"
? t("home:sidebar.apps")
: item.key === "chat"
? t("home:sidebar.chat")
: t("settings:title");
return ( return (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.key}>
<SidebarMenuButton <SidebarMenuButton
as={Link} as={Link}
to={item.to} to={item.to}
...@@ -206,20 +196,18 @@ function AppIcons({ ...@@ -206,20 +196,18 @@ function AppIcons({
isActive ? "bg-sidebar-accent" : "" isActive ? "bg-sidebar-accent" : ""
}`} }`}
onMouseEnter={() => { onMouseEnter={() => {
if (item.title === "Apps") { if (item.key === "apps") {
onHoverChange("start-hover:app"); onHoverChange("start-hover:app");
} else if (item.title === "Chat") { } else if (item.key === "chat") {
onHoverChange("start-hover:chat"); onHoverChange("start-hover:chat");
} else if (item.title === "Settings") { } else if (item.key === "settings") {
onHoverChange("start-hover:settings"); onHoverChange("start-hover:settings");
} else if (item.title === "Library") {
onHoverChange("start-hover:library");
} }
}} }}
> >
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<item.icon className="h-5 w-5" /> <item.icon className="h-5 w-5" />
<span className={"text-xs"}>{item.title}</span> <span className={"text-xs"}>{label}</span>
</div> </div>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
......
...@@ -38,12 +38,14 @@ import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo"; ...@@ -38,12 +38,14 @@ import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo";
import { ipc } from "@/ipc/types"; import { ipc } from "@/ipc/types";
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
import { useTranslation } from "react-i18next";
export function HomeChatInput({ export function HomeChatInput({
onSubmit, onSubmit,
}: { }: {
onSubmit: (options?: HomeSubmitOptions) => void; onSubmit: (options?: HomeSubmitOptions) => void;
}) { }) {
const { t } = useTranslation(["chat", "home"]);
const posthog = usePostHog(); const posthog = usePostHog();
const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom); const [inputValue, setInputValue] = useAtom(homeChatInputValueAtom);
const [selectedApp, setSelectedApp] = useAtom(homeSelectedAppAtom); const [selectedApp, setSelectedApp] = useAtom(homeSelectedAppAtom);
...@@ -221,10 +223,10 @@ export function HomeChatInput({ ...@@ -221,10 +223,10 @@ export function HomeChatInput({
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{isRecording {isRecording
? "Stop recording" ? t("chat:stopRecording")
: isTranscribing : isTranscribing
? "Transcribing..." ? t("chat:transcribing")
: "Voice to text"} : t("chat:voiceToText")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
) : ( ) : (
...@@ -235,7 +237,7 @@ export function HomeChatInput({ ...@@ -235,7 +237,7 @@ export function HomeChatInput({
onClick={() => onClick={() =>
ipc.system.openExternalUrl("https://dyad.sh/pro") ipc.system.openExternalUrl("https://dyad.sh/pro")
} }
aria-label="Voice to text (Pro)" aria-label={t("chat:voiceToTextPro")}
className="px-2 py-2 mb-0.5 text-muted-foreground hover:text-primary rounded-lg transition-colors duration-150 cursor-pointer relative" className="px-2 py-2 mb-0.5 text-muted-foreground hover:text-primary rounded-lg transition-colors duration-150 cursor-pointer relative"
/> />
} }
...@@ -243,7 +245,7 @@ export function HomeChatInput({ ...@@ -243,7 +245,7 @@ export function HomeChatInput({
<Mic size={20} /> <Mic size={20} />
<Lock size={10} className="absolute -top-0.5 -right-0.5" /> <Lock size={10} className="absolute -top-0.5 -right-0.5" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Voice to text (requires Pro)</TooltipContent> <TooltipContent>{t("chat:voiceToTextRequiresPro")}</TooltipContent>
</Tooltip> </Tooltip>
)} )}
...@@ -252,16 +254,14 @@ export function HomeChatInput({ ...@@ -252,16 +254,14 @@ export function HomeChatInput({
<TooltipTrigger <TooltipTrigger
render={ render={
<button <button
aria-label="Cancel generation (unavailable here)" aria-label={t("chat:cancelGenerationUnavailable")}
className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground rounded-lg opacity-50 cursor-not-allowed transition-colors duration-150" className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground rounded-lg opacity-50 cursor-not-allowed transition-colors duration-150"
/> />
} }
> >
<StopCircleIcon size={20} /> <StopCircleIcon size={20} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>{t("chat:cancelGenerationUnavailable")}</TooltipContent>
Cancel generation (unavailable here)
</TooltipContent>
</Tooltip> </Tooltip>
) : ( ) : (
<Tooltip> <Tooltip>
...@@ -270,14 +270,14 @@ export function HomeChatInput({ ...@@ -270,14 +270,14 @@ export function HomeChatInput({
<button <button
onClick={handleCustomSubmit} onClick={handleCustomSubmit}
disabled={!inputValue.trim() && attachments.length === 0} disabled={!inputValue.trim() && attachments.length === 0}
aria-label="Send message" aria-label={t("chat:sendMessage")}
className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground hover:text-primary rounded-lg transition-colors duration-150 disabled:opacity-30 disabled:hover:text-muted-foreground cursor-pointer disabled:cursor-default" className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground hover:text-primary rounded-lg transition-colors duration-150 disabled:opacity-30 disabled:hover:text-muted-foreground cursor-pointer disabled:cursor-default"
/> />
} }
> >
<SendHorizontalIcon size={20} /> <SendHorizontalIcon size={20} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Send message</TooltipContent> <TooltipContent>{t("chat:sendMessage")}</TooltipContent>
</Tooltip> </Tooltip>
)} )}
</div> </div>
...@@ -302,7 +302,7 @@ export function HomeChatInput({ ...@@ -302,7 +302,7 @@ export function HomeChatInput({
> >
<FolderOpenIcon size={14} /> <FolderOpenIcon size={14} />
<span className="truncate max-w-[150px]"> <span className="truncate max-w-[150px]">
{selectedApp ? selectedApp.name : "No app selected"} {selectedApp ? selectedApp.name : t("home:noAppSelected")}
</span> </span>
{selectedApp && ( {selectedApp && (
<button <button
...@@ -312,7 +312,7 @@ export function HomeChatInput({ ...@@ -312,7 +312,7 @@ export function HomeChatInput({
setSelectedApp(null); setSelectedApp(null);
}} }}
className="hover:bg-primary/20 rounded-sm p-0.5 transition-colors" className="hover:bg-primary/20 rounded-sm p-0.5 transition-colors"
aria-label="Deselect app" aria-label={t("home:deselectApp")}
data-testid="home-app-selector-clear" data-testid="home-app-selector-clear"
> >
<XIcon size={12} /> <XIcon size={12} />
...@@ -321,8 +321,8 @@ export function HomeChatInput({ ...@@ -321,8 +321,8 @@ export function HomeChatInput({
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{selectedApp {selectedApp
? "Change selected app" ? t("home:changeSelectedApp")
: "Select an existing app"} : t("home:selectExistingApp")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)} )}
......
...@@ -19,7 +19,6 @@ import { chatMessagesByIdAtom } from "@/atoms/chatAtoms"; ...@@ -19,7 +19,6 @@ import { chatMessagesByIdAtom } from "@/atoms/chatAtoms";
import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders"; import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo"; import { useUserBudgetInfo } from "@/hooks/useUserBudgetInfo";
import { PromoMessage } from "./PromoMessage";
import { isCancelledResponseContent } from "@/shared/chatCancellation"; import { isCancelledResponseContent } from "@/shared/chatCancellation";
interface MessagesListProps { interface MessagesListProps {
...@@ -251,14 +250,6 @@ function FooterComponent({ context }: { context?: FooterContext }) { ...@@ -251,14 +250,6 @@ function FooterComponent({ context }: { context?: FooterContext }) {
</div> </div>
</div> </div>
)} )}
{isStreaming &&
!settings?.enableDyadPro &&
!userBudget &&
messages.length > 0 && (
<PromoMessage
seed={messages.length * (appId ?? 1) * (selectedChatId ?? 1)}
/>
)}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
{renderSetupBanner()} {renderSetupBanner()}
</> </>
......
...@@ -198,8 +198,6 @@ export const GITHUB_TIP: MessageConfig = { ...@@ -198,8 +198,6 @@ export const GITHUB_TIP: MessageConfig = {
}; };
// Array of all available messages for rotation // Array of all available messages for rotation
const ALL_MESSAGES = [ const ALL_MESSAGES = [
TURBO_EDITS_PROMO_MESSAGE,
SMART_CONTEXT_PROMO_MESSAGE,
DIFFERENT_MODEL_TIP, DIFFERENT_MODEL_TIP,
REDDIT_TIP, REDDIT_TIP,
REPORT_A_BUG_TIP, REPORT_A_BUG_TIP,
......
...@@ -48,7 +48,7 @@ const resources = { ...@@ -48,7 +48,7 @@ const resources = {
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
resources, resources,
lng: "en", // Default; overridden by user setting on startup lng: "zh-CN", // Default; overridden by user setting on startup
fallbackLng: "en", fallbackLng: "en",
defaultNS: "common", defaultNS: "common",
ns: ["common", "settings", "chat", "home", "errors"], ns: ["common", "settings", "chat", "home", "errors"],
......
...@@ -27,7 +27,13 @@ ...@@ -27,7 +27,13 @@
"scrollToBottom": "Scroll to bottom", "scrollToBottom": "Scroll to bottom",
"dismissError": "Dismiss error", "dismissError": "Dismiss error",
"askDyadToBuild": "Ask Dyad to build...", "askDyadToBuild": "Ask Dyad to build...",
"voiceToText": "Voice to text",
"voiceToTextPro": "Voice to text (Pro)",
"voiceToTextRequiresPro": "Voice to text (requires Pro)",
"stopRecording": "Stop recording",
"transcribing": "Transcribing...",
"cancelGeneration": "Cancel generation", "cancelGeneration": "Cancel generation",
"cancelGenerationUnavailable": "Cancel generation (unavailable here)",
"sendMessage": "Send message", "sendMessage": "Send message",
"loadingProposal": "Loading proposal...", "loadingProposal": "Loading proposal...",
"errorLoadingProposal": "Error loading proposal: {{message}}", "errorLoadingProposal": "Error loading proposal: {{message}}",
......
...@@ -23,6 +23,10 @@ ...@@ -23,6 +23,10 @@
"selectedFolder": "Selected folder:", "selectedFolder": "Selected folder:",
"clearSelection": "Clear selection", "clearSelection": "Clear selection",
"copyToDyadApps": "Copy to the dyad-apps folder", "copyToDyadApps": "Copy to the dyad-apps folder",
"noAppSelected": "No app selected",
"deselectApp": "Deselect app",
"changeSelectedApp": "Change selected app",
"selectExistingApp": "Select an existing app",
"appNameExists": "An app with this name already exists. Please choose a different name:", "appNameExists": "An app with this name already exists. Please choose a different name:",
"appName": "App name", "appName": "App name",
"appNameOptional": "App name (optional)", "appNameOptional": "App name (optional)",
...@@ -31,7 +35,10 @@ ...@@ -31,7 +35,10 @@
"advancedOptions": "Advanced options", "advancedOptions": "Advanced options",
"installCommand": "Install command", "installCommand": "Install command",
"startCommand": "Start command", "startCommand": "Start command",
"installCommandPlaceholder": "pnpm install",
"startCommandPlaceholder": "pnpm dev",
"bothCommandsRequired": "Both commands are required when customizing.", "bothCommandsRequired": "Both commands are required when customizing.",
"aiRulesTooltip": "AI_RULES.md lets Dyad know which tech stack to use for editing the app",
"noAiRulesFound": "No AI_RULES.md found. Dyad will automatically generate one after importing.", "noAiRulesFound": "No AI_RULES.md found. Dyad will automatically generate one after importing.",
"importingApp": "Importing app...", "importingApp": "Importing app...",
"import": "Import", "import": "Import",
...@@ -60,6 +67,34 @@ ...@@ -60,6 +67,34 @@
"communityCodeWarning": "This code was created by a Dyad community member, not our core team.", "communityCodeWarning": "This code was created by a Dyad community member, not our core team.",
"communityCodeRisk": "Community code can be very helpful, but since it's built independently, it may have bugs, security risks, or could cause issues with your system. We can't provide official support if problems occur.", "communityCodeRisk": "Community code can be very helpful, but since it's built independently, it may have bugs, security risks, or could cause issues with your system. We can't provide official support if problems occur.",
"communityCodeReview": "We recommend reviewing the code on GitHub first. Only proceed if you're comfortable with these risks.", "communityCodeReview": "We recommend reviewing the code on GitHub first. Only proceed if you're comfortable with these risks.",
"apps": {
"title": "Apps",
"goBack": "Go Back",
"searchPlaceholder": "Search apps...",
"searchAriaLabel": "Search apps",
"searchDialogPlaceholder": "Search apps",
"loading": "Loading apps...",
"noSearchResults": "No apps match your search.",
"empty": "You haven't created any apps yet.",
"createFirst": "Create your first app",
"noResults": "No results found."
},
"appList": {
"title": "Your Apps",
"newApp": "New App",
"searchApps": "Search Apps",
"loading": "Loading apps...",
"error": "Error loading apps",
"empty": "No apps found",
"favorites": "Favorite apps",
"noFavorites": "Star an app from its details page to pin it here",
"others": "Other apps"
},
"sidebar": {
"apps": "Apps",
"chat": "Chat",
"help": "Help"
},
"deleteItemTitle": "Delete {{itemType}}", "deleteItemTitle": "Delete {{itemType}}",
"deleteItemConfirmation": "Are you sure you want to delete \"{{itemName}}\"? This action cannot be undone.", "deleteItemConfirmation": "Are you sure you want to delete \"{{itemName}}\"? This action cannot be undone.",
"proBanner": { "proBanner": {
...@@ -84,7 +119,10 @@ ...@@ -84,7 +119,10 @@
"moreDownloadOptions": "more download options", "moreDownloadOptions": "more download options",
"nodeAlreadyInstalled": "Node.js already installed? Configure path manually", "nodeAlreadyInstalled": "Node.js already installed? Configure path manually",
"browseForNodeFolder": "Browse for Node.js folder", "browseForNodeFolder": "Browse for Node.js folder",
"couldNotFindNodeAtPath": "Could not find Node.js at the path \"{{path}}\"",
"errorSettingNodePath": "Error setting Node.js path: {{error}}",
"nodeTroubleshooting": "Node.js troubleshooting guide", "nodeTroubleshooting": "Node.js troubleshooting guide",
"ifYouRunIntoIssues": "If you run into issues, read our",
"stillStuck": "Still stuck? Click the Help button in the bottom-left corner and then Report a Bug.", "stillStuck": "Still stuck? Click the Help button in the bottom-left corner and then Report a Bug.",
"installNodeRuntime": "Install Node.js Runtime", "installNodeRuntime": "Install Node.js Runtime",
"checkingNodeSetup": "Checking Node.js setup...", "checkingNodeSetup": "Checking Node.js setup...",
......
...@@ -27,7 +27,13 @@ ...@@ -27,7 +27,13 @@
"scrollToBottom": "滚动到底部", "scrollToBottom": "滚动到底部",
"dismissError": "忽略错误", "dismissError": "忽略错误",
"askDyadToBuild": "让 Dyad 来构建...", "askDyadToBuild": "让 Dyad 来构建...",
"voiceToText": "语音转文字",
"voiceToTextPro": "语音转文字(Pro)",
"voiceToTextRequiresPro": "语音转文字(需要 Pro)",
"stopRecording": "停止录音",
"transcribing": "转录中...",
"cancelGeneration": "取消生成", "cancelGeneration": "取消生成",
"cancelGenerationUnavailable": "此处无法取消生成",
"sendMessage": "发送消息", "sendMessage": "发送消息",
"loadingProposal": "正在加载提案...", "loadingProposal": "正在加载提案...",
"errorLoadingProposal": "加载提案出错:{{message}}", "errorLoadingProposal": "加载提案出错:{{message}}",
......
...@@ -20,6 +20,10 @@ ...@@ -20,6 +20,10 @@
"selectedFolder": "已选择文件夹:", "selectedFolder": "已选择文件夹:",
"clearSelection": "清除选择", "clearSelection": "清除选择",
"copyToDyadApps": "复制到 dyad-apps 文件夹", "copyToDyadApps": "复制到 dyad-apps 文件夹",
"noAppSelected": "未选择应用",
"deselectApp": "取消选择应用",
"changeSelectedApp": "更换已选择的应用",
"selectExistingApp": "选择现有应用",
"appNameExists": "此名称的应用已存在。请选择其他名称:", "appNameExists": "此名称的应用已存在。请选择其他名称:",
"appName": "应用名称", "appName": "应用名称",
"appNameOptional": "应用名称(可选)", "appNameOptional": "应用名称(可选)",
...@@ -28,7 +32,10 @@ ...@@ -28,7 +32,10 @@
"advancedOptions": "高级选项", "advancedOptions": "高级选项",
"installCommand": "安装命令", "installCommand": "安装命令",
"startCommand": "启动命令", "startCommand": "启动命令",
"installCommandPlaceholder": "pnpm install",
"startCommandPlaceholder": "pnpm dev",
"bothCommandsRequired": "自定义时两个命令都是必填的。", "bothCommandsRequired": "自定义时两个命令都是必填的。",
"aiRulesTooltip": "AI_RULES.md 会告诉 Dyad 在编辑此应用时应使用哪种技术栈",
"noAiRulesFound": "未找到 AI_RULES.md。导入后 Dyad 将自动生成一个。", "noAiRulesFound": "未找到 AI_RULES.md。导入后 Dyad 将自动生成一个。",
"importingApp": "正在导入应用...", "importingApp": "正在导入应用...",
"import": "导入", "import": "导入",
...@@ -57,6 +64,34 @@ ...@@ -57,6 +64,34 @@
"communityCodeWarning": "此代码由 Dyad 社区成员创建,并非我们的核心团队。", "communityCodeWarning": "此代码由 Dyad 社区成员创建,并非我们的核心团队。",
"communityCodeRisk": "社区代码可能非常有用,但由于是独立构建的,可能存在缺陷、安全风险或导致系统问题。如果出现问题,我们无法提供官方支持。", "communityCodeRisk": "社区代码可能非常有用,但由于是独立构建的,可能存在缺陷、安全风险或导致系统问题。如果出现问题,我们无法提供官方支持。",
"communityCodeReview": "我们建议先在 GitHub 上查看代码。只有在您了解这些风险后再继续。", "communityCodeReview": "我们建议先在 GitHub 上查看代码。只有在您了解这些风险后再继续。",
"apps": {
"title": "应用",
"goBack": "返回",
"searchPlaceholder": "搜索应用...",
"searchAriaLabel": "搜索应用",
"searchDialogPlaceholder": "搜索应用",
"loading": "正在加载应用...",
"noSearchResults": "没有应用匹配您的搜索。",
"empty": "您还没有创建任何应用。",
"createFirst": "创建您的第一个应用",
"noResults": "未找到结果"
},
"appList": {
"title": "我的应用",
"newApp": "新建应用",
"searchApps": "搜索应用",
"loading": "正在加载应用...",
"error": "加载应用时出错",
"empty": "未找到应用",
"favorites": "收藏的应用",
"noFavorites": "在应用详情页为应用加星,即可将其固定到这里",
"others": "其他应用"
},
"sidebar": {
"apps": "应用",
"chat": "聊天",
"help": "帮助"
},
"deleteItemTitle": "删除{{itemType}}", "deleteItemTitle": "删除{{itemType}}",
"deleteItemConfirmation": "确定要删除「{{itemName}}」吗?此操作无法撤销。", "deleteItemConfirmation": "确定要删除「{{itemName}}」吗?此操作无法撤销。",
"proBanner": { "proBanner": {
...@@ -81,7 +116,10 @@ ...@@ -81,7 +116,10 @@
"moreDownloadOptions": "更多下载选项", "moreDownloadOptions": "更多下载选项",
"nodeAlreadyInstalled": "Node.js 已安装?手动配置路径", "nodeAlreadyInstalled": "Node.js 已安装?手动配置路径",
"browseForNodeFolder": "浏览 Node.js 文件夹", "browseForNodeFolder": "浏览 Node.js 文件夹",
"couldNotFindNodeAtPath": "在路径“{{path}}”中找不到 Node.js",
"errorSettingNodePath": "设置 Node.js 路径时出错:{{error}}",
"nodeTroubleshooting": "Node.js 故障排除指南", "nodeTroubleshooting": "Node.js 故障排除指南",
"ifYouRunIntoIssues": "如果您遇到问题,请阅读我们的",
"stillStuck": "仍然遇到问题?点击左下角的「帮助」按钮,然后点击「报告 Bug」。", "stillStuck": "仍然遇到问题?点击左下角的「帮助」按钮,然后点击「报告 Bug」。",
"installNodeRuntime": "安装 Node.js 运行时", "installNodeRuntime": "安装 Node.js 运行时",
"checkingNodeSetup": "正在检查 Node.js 设置...", "checkingNodeSetup": "正在检查 Node.js 设置...",
......
...@@ -49,6 +49,7 @@ const DEFAULT_SETTINGS: UserSettings = { ...@@ -49,6 +49,7 @@ const DEFAULT_SETTINGS: UserSettings = {
autoExpandPreviewPanel: true, autoExpandPreviewPanel: true,
enableContextCompaction: true, enableContextCompaction: true,
previewIdleTimeoutPolicy: "default", previewIdleTimeoutPolicy: "default",
language: "zh-CN",
}; };
const SETTINGS_FILE = "user-settings.json"; const SETTINGS_FILE = "user-settings.json";
......
...@@ -8,8 +8,10 @@ import { useOpenApp } from "@/hooks/useOpenApp"; ...@@ -8,8 +8,10 @@ import { useOpenApp } from "@/hooks/useOpenApp";
import { AppShowcaseCard } from "@/components/AppShowcaseCard"; import { AppShowcaseCard } from "@/components/AppShowcaseCard";
import { useAppThumbnails } from "@/hooks/useAppThumbnails"; import { useAppThumbnails } from "@/hooks/useAppThumbnails";
import { sortAppsForShowcase } from "@/lib/sortApps"; import { sortAppsForShowcase } from "@/lib/sortApps";
import { useTranslation } from "react-i18next";
export default function AppsPage() { export default function AppsPage() {
const { t } = useTranslation("home");
const router = useRouter(); const router = useRouter();
const navigate = useNavigate(); const navigate = useNavigate();
const { apps, loading } = useLoadApps(); const { apps, loading } = useLoadApps();
...@@ -48,11 +50,11 @@ export default function AppsPage() { ...@@ -48,11 +50,11 @@ export default function AppsPage() {
className="flex items-center gap-2 mb-4 bg-(--background-lightest) py-5" className="flex items-center gap-2 mb-4 bg-(--background-lightest) py-5"
> >
<ArrowLeft className="h-4 w-4" /> <ArrowLeft className="h-4 w-4" />
Go Back {t("apps.goBack")}
</Button> </Button>
<header className="mb-6 text-left"> <header className="mb-6 text-left">
<h1 className="text-3xl font-bold mb-2">Apps</h1> <h1 className="text-3xl font-bold mb-2">{t("apps.title")}</h1>
</header> </header>
<div className="mb-6"> <div className="mb-6">
...@@ -66,8 +68,8 @@ export default function AppsPage() { ...@@ -66,8 +68,8 @@ export default function AppsPage() {
<Search className="absolute left-4 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-4 h-4 w-4 text-muted-foreground" />
<input <input
type="text" type="text"
placeholder="Search apps..." placeholder={t("apps.searchPlaceholder")}
aria-label="Search apps" aria-label={t("apps.searchAriaLabel")}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-transparent py-3 pl-11 pr-4 text-sm outline-none placeholder:text-muted-foreground" className="w-full bg-transparent py-3 pl-11 pr-4 text-sm outline-none placeholder:text-muted-foreground"
...@@ -77,18 +79,18 @@ export default function AppsPage() { ...@@ -77,18 +79,18 @@ export default function AppsPage() {
{loading ? ( {loading ? (
<div className="text-muted-foreground text-center py-12"> <div className="text-muted-foreground text-center py-12">
Loading apps... {t("apps.loading")}
</div> </div>
) : filteredApps.length === 0 ? ( ) : filteredApps.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 gap-3"> <div className="flex flex-col items-center justify-center py-16 gap-3">
<p className="text-muted-foreground text-center"> <p className="text-muted-foreground text-center">
{searchQuery {searchQuery
? "No apps match your search." ? t("apps.noSearchResults")
: "You haven't created any apps yet."} : t("apps.empty")}
</p> </p>
{!searchQuery && ( {!searchQuery && (
<Button onClick={() => navigate({ to: "/" })} size="sm"> <Button onClick={() => navigate({ to: "/" })} size="sm">
Create your first app {t("apps.createFirst")}
</Button> </Button>
)} )}
</div> </div>
......
...@@ -8,12 +8,10 @@ import { useLoadApps } from "@/hooks/useLoadApps"; ...@@ -8,12 +8,10 @@ import { useLoadApps } from "@/hooks/useLoadApps";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { SetupBanner } from "@/components/SetupBanner"; import { SetupBanner } from "@/components/SetupBanner";
import { isPreviewOpenAtom } from "@/atoms/viewAtoms"; import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useStreamChat } from "@/hooks/useStreamChat"; import { useStreamChat } from "@/hooks/useStreamChat";
import { HomeChatInput } from "@/components/chat/HomeChatInput"; import { HomeChatInput } from "@/components/chat/HomeChatInput";
import { usePostHog } from "posthog-js/react"; import { usePostHog } from "posthog-js/react";
import { PrivacyBanner } from "@/components/TelemetryBanner";
import { INSPIRATION_PROMPTS } from "@/prompts/inspiration_prompts";
import { useAppVersion } from "@/hooks/useAppVersion"; import { useAppVersion } from "@/hooks/useAppVersion";
import { import {
...@@ -32,18 +30,12 @@ import { useQueryClient } from "@tanstack/react-query"; ...@@ -32,18 +30,12 @@ import { useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "@/lib/queryKeys"; import { queryKeys } from "@/lib/queryKeys";
import { ForceCloseDialog } from "@/components/ForceCloseDialog"; import { ForceCloseDialog } from "@/components/ForceCloseDialog";
import { useSelectChat } from "@/hooks/useSelectChat"; import { useSelectChat } from "@/hooks/useSelectChat";
import { FeaturedAppShowcase } from "@/components/FeaturedAppShowcase";
import type { FileAttachment } from "@/ipc/types"; import type { FileAttachment } from "@/ipc/types";
import type { ListedApp } from "@/ipc/types/app"; import type { ListedApp } from "@/ipc/types/app";
import { NEON_TEMPLATE_IDS } from "@/shared/templates"; import { NEON_TEMPLATE_IDS } from "@/shared/templates";
import { neonTemplateHook } from "@/client_logic/template_hook"; import { neonTemplateHook } from "@/client_logic/template_hook";
import { import { getEffectiveDefaultChatMode } from "@/lib/schemas";
ProBanner,
ManageDyadProButton,
SetupDyadProButton,
} from "@/components/ProBanner";
import { hasDyadProKey, getEffectiveDefaultChatMode } from "@/lib/schemas";
import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota"; import { useFreeAgentQuota } from "@/hooks/useFreeAgentQuota";
import { useInitialChatMode } from "@/hooks/useInitialChatMode"; import { useInitialChatMode } from "@/hooks/useInitialChatMode";
...@@ -134,22 +126,6 @@ export default function HomePage() { ...@@ -134,22 +126,6 @@ export default function HomePage() {
// Get the appId from search params // Get the appId from search params
const appId = search.appId ? Number(search.appId) : null; const appId = search.appId ? Number(search.appId) : null;
// State for random prompts
const [randomPrompts, setRandomPrompts] = useState<
typeof INSPIRATION_PROMPTS
>([]);
// Function to get random prompts
const getRandomPrompts = useCallback(() => {
const shuffled = [...INSPIRATION_PROMPTS].sort(() => 0.5 - Math.random());
return shuffled.slice(0, 3);
}, []);
// Initialize random prompts
useEffect(() => {
setRandomPrompts(getRandomPrompts());
}, [getRandomPrompts]);
// Redirect to app details page if appId is present // Redirect to app details page if appId is present
useEffect(() => { useEffect(() => {
if (appId) { if (appId) {
...@@ -286,13 +262,6 @@ export default function HomePage() { ...@@ -286,13 +262,6 @@ export default function HomePage() {
return ( return (
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className="flex flex-col items-center justify-center max-w-3xl w-full m-auto p-8 relative"> <div className="flex flex-col items-center justify-center max-w-3xl w-full m-auto p-8 relative">
<div className="fixed top-16 right-8 z-50">
{settings && hasDyadProKey(settings) ? (
<ManageDyadProButton className="mt-0 w-auto h-9 px-3 text-base shadow-sm bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm hover:bg-white dark:hover:bg-gray-800" />
) : (
<SetupDyadProButton />
)}
</div>
<ForceCloseDialog <ForceCloseDialog
isOpen={forceCloseDialogOpen} isOpen={forceCloseDialogOpen}
onClose={() => setForceCloseDialogOpen(false)} onClose={() => setForceCloseDialogOpen(false)}
...@@ -305,66 +274,7 @@ export default function HomePage() { ...@@ -305,66 +274,7 @@ export default function HomePage() {
<ImportAppButton className="px-0 pb-0 flex-none" /> <ImportAppButton className="px-0 pb-0 flex-none" />
</div> </div>
<HomeChatInput onSubmit={handleSubmit} /> <HomeChatInput onSubmit={handleSubmit} />
<div className="flex flex-col gap-4 mt-2">
<div className="flex flex-wrap gap-4 justify-center">
{randomPrompts.map((item, index) => (
<button
type="button"
key={index}
onClick={() =>
setInputValue(t("buildMeA", { label: item.label }))
}
className="flex items-center gap-3 px-4 py-2 rounded-xl border border-gray-200
bg-white/50 backdrop-blur-sm
transition-all duration-200
hover:bg-white hover:shadow-md hover:border-gray-300
active:scale-[0.98]
dark:bg-gray-800/50 dark:border-gray-700
dark:hover:bg-gray-800 dark:hover:border-gray-600"
>
<span className="text-gray-700 dark:text-gray-300">
{item.icon}
</span>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{item.label}
</span>
</button>
))}
</div>
<button
type="button"
onClick={() => setRandomPrompts(getRandomPrompts())}
className="self-center flex items-center gap-2 px-4 py-2 rounded-xl border border-gray-200
bg-white/50 backdrop-blur-sm
transition-all duration-200
hover:bg-white hover:shadow-md hover:border-gray-300
active:scale-[0.98]
dark:bg-gray-800/50 dark:border-gray-700
dark:hover:bg-gray-800 dark:hover:border-gray-600"
>
<svg
className="w-5 h-5 text-gray-700 dark:text-gray-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{t("moreIdeas")}
</span>
</button>
</div>
<ProBanner />
</div> </div>
<PrivacyBanner />
{/* Release Notes Dialog */} {/* Release Notes Dialog */}
<Dialog open={releaseNotesOpen} onOpenChange={setReleaseNotesOpen}> <Dialog open={releaseNotesOpen} onOpenChange={setReleaseNotesOpen}>
...@@ -401,7 +311,6 @@ export default function HomePage() { ...@@ -401,7 +311,6 @@ export default function HomePage() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>
<FeaturedAppShowcase />
</div> </div>
); );
} }
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论