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