Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
643 changes: 643 additions & 0 deletions .claude/plan/i18n-internationalization.md

Large diffs are not rendered by default.

20 changes: 8 additions & 12 deletions frontend/app/aipanel/aifeedbackbuttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@

import { cn, makeIconClass } from "@/util/util";
import { memo, useState } from "react";
import { useTranslation } from "react-i18next";
import { WaveAIModel } from "./waveai-model";

interface AIFeedbackButtonsProps {
messageText: string;
}

export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps) => {
const { t } = useTranslation("ai");
const [thumbsUpClicked, setThumbsUpClicked] = useState(false);
const [thumbsDownClicked, setThumbsDownClicked] = useState(false);
const [copied, setCopied] = useState(false);
Expand Down Expand Up @@ -46,23 +48,19 @@ export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps)
onClick={handleThumbsUp}
className={cn(
"p-1.5 rounded cursor-pointer transition-colors",
thumbsUpClicked
? "text-accent"
: "text-secondary hover:bg-gray-700 hover:text-primary"
thumbsUpClicked ? "text-accent" : "text-secondary hover:bg-gray-700 hover:text-primary"
)}
title="Good Response"
title={t("message.goodResponse")}
>
<i className={makeIconClass(thumbsUpClicked ? "solid@thumbs-up" : "regular@thumbs-up", false)} />
</button>
<button
onClick={handleThumbsDown}
className={cn(
"p-1.5 rounded cursor-pointer transition-colors",
thumbsDownClicked
? "text-accent"
: "text-secondary hover:bg-gray-700 hover:text-primary"
thumbsDownClicked ? "text-accent" : "text-secondary hover:bg-gray-700 hover:text-primary"
)}
title="Bad Response"
title={t("message.badResponse")}
>
<i className={makeIconClass(thumbsDownClicked ? "solid@thumbs-down" : "regular@thumbs-down", false)} />
</button>
Expand All @@ -71,11 +69,9 @@ export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps)
onClick={handleCopy}
className={cn(
"p-1.5 rounded cursor-pointer transition-colors",
copied
? "text-success"
: "text-secondary hover:bg-gray-700 hover:text-primary"
copied ? "text-success" : "text-secondary hover:bg-gray-700 hover:text-primary"
)}
title="Copy Message"
title={t("message.copyMessage")}
>
<i className={makeIconClass(copied ? "solid@check" : "regular@copy", false)} />
</button>
Expand Down
14 changes: 8 additions & 6 deletions frontend/app/aipanel/aimessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { WaveStreamdown } from "@/app/element/streamdown";
import { cn } from "@/util/util";
import { memo, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { getFileIcon } from "./ai-utils";
import { AIFeedbackButtons } from "./aifeedbackbuttons";
import { AIToolUseGroup } from "./aitooluse";
Expand Down Expand Up @@ -180,7 +181,7 @@ const getThinkingMessage = (
parts: WaveUIMessagePart[],
isStreaming: boolean,
role: string
): { message: string; reasoningText?: string; isWaitingApproval?: boolean } | null => {
): { messageKey: string; reasoningText?: string; isWaitingApproval?: boolean } | null => {
if (!isStreaming || role !== "assistant") {
return null;
}
Expand All @@ -190,24 +191,25 @@ const getThinkingMessage = (
);

if (hasPendingApprovals) {
return { message: "Waiting for Tool Approvals...", isWaitingApproval: true };
return { messageKey: "message.waitingApproval", isWaitingApproval: true };
}

const lastPart = parts[parts.length - 1];

if (lastPart?.type === "reasoning") {
const reasoningContent = lastPart.text || "";
return { message: "AI is thinking...", reasoningText: reasoningContent };
return { messageKey: "message.thinking", reasoningText: reasoningContent };
}

if (lastPart?.type === "text" && lastPart.text) {
return null;
}

return { message: "" };
return { messageKey: "" };
};

export const AIMessage = memo(({ message, isStreaming }: AIMessageProps) => {
const { t } = useTranslation("ai");
const parts = message.parts || [];
const displayParts = parts.filter(isDisplayPart);
const fileParts = parts.filter(
Expand All @@ -228,7 +230,7 @@ export const AIMessage = memo(({ message, isStreaming }: AIMessageProps) => {
)}
>
{displayParts.length === 0 && !isStreaming && !thinkingData ? (
<div className="whitespace-pre-wrap break-words">(no text content)</div>
<div className="whitespace-pre-wrap break-words">{t("message.noContent")}</div>
) : (
<>
{groupedParts.map((group, index: number) =>
Expand All @@ -243,7 +245,7 @@ export const AIMessage = memo(({ message, isStreaming }: AIMessageProps) => {
{thinkingData != null && (
<div className="mt-2">
<AIThinking
message={thinkingData.message}
message={thinkingData.messageKey ? t(thinkingData.messageKey) : ""}
reasoningText={thinkingData.reasoningText}
isWaitingApproval={thinkingData.isWaitingApproval}
/>
Expand Down
6 changes: 4 additions & 2 deletions frontend/app/aipanel/aipanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { DefaultChatTransport } from "ai";
import * as jotai from "jotai";
import { memo, useCallback, useEffect, useRef, useState } from "react";
import { useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import { formatFileSizeError, isAcceptableFile, validateFileSize } from "./ai-utils";
import { AIDroppedFiles } from "./aidroppedfiles";
import { AIModeDropdown } from "./aimode";
Expand Down Expand Up @@ -51,15 +52,16 @@ const AIBlockMask = memo(() => {
AIBlockMask.displayName = "AIBlockMask";

const AIDragOverlay = memo(() => {
const { t } = useTranslation("ai");
return (
<div
key="drag-overlay"
className="absolute inset-0 bg-accent/20 border-2 border-dashed border-accent rounded-lg flex items-center justify-center z-10 p-4"
>
<div className="text-accent text-center">
<i className="fa fa-upload text-3xl mb-2"></i>
<div className="text-lg font-semibold">Drop files here</div>
<div className="text-sm">Images, PDFs, and text/code files supported</div>
<div className="text-lg font-semibold">{t("dragDrop.title")}</div>
<div className="text-sm">{t("dragDrop.supported")}</div>
</div>
</div>
);
Expand Down
14 changes: 9 additions & 5 deletions frontend/app/aipanel/aipanelheader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import { handleWaveAIContextMenu } from "@/app/aipanel/aipanel-contextmenu";
import { useAtomValue } from "jotai";
import { memo } from "react";
import { useTranslation } from "react-i18next";
import { WaveAIModel } from "./waveai-model";

export const AIPanelHeader = memo(() => {
const { t } = useTranslation("ai");
const model = WaveAIModel.getInstance();
const widgetAccess = useAtomValue(model.widgetAccessAtom);
const inBuilder = model.inBuilder;
Expand All @@ -26,14 +28,16 @@ export const AIPanelHeader = memo(() => {
>
<h2 className="text-white text-sm @xs:text-lg font-semibold flex items-center gap-2 flex-shrink-0 whitespace-nowrap">
<i className="fa fa-sparkles text-accent"></i>
Wave AI
{t("panel.title")}
</h2>

<div className="flex items-center flex-shrink-0 whitespace-nowrap">
{!inBuilder && (
<div className="flex items-center text-sm whitespace-nowrap">
<span className="text-gray-300 @xs:hidden mr-1 text-[12px]">Context</span>
<span className="text-gray-300 hidden @xs:inline mr-2 text-[12px]">Widget Context</span>
<span className="text-gray-300 @xs:hidden mr-1 text-[12px]">{t("header.context")}</span>
<span className="text-gray-300 hidden @xs:inline mr-2 text-[12px]">
{t("header.widgetContext")}
</span>
<button
onClick={() => {
model.setWidgetAccess(!widgetAccess);
Expand All @@ -44,7 +48,7 @@ export const AIPanelHeader = memo(() => {
className={`relative inline-flex h-6 w-14 items-center rounded-full transition-colors cursor-pointer ${
widgetAccess ? "bg-accent-500" : "bg-gray-600"
}`}
title={`Widget Access ${widgetAccess ? "ON" : "OFF"}`}
title={widgetAccess ? t("header.widgetAccessOn") : t("header.widgetAccessOff")}
>
<span
className={`absolute inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
Expand All @@ -65,7 +69,7 @@ export const AIPanelHeader = memo(() => {
<button
onClick={handleKebabClick}
className="text-gray-400 hover:text-white cursor-pointer transition-colors p-1 rounded flex-shrink-0 ml-2 focus:outline-none"
title="More options"
title={t("header.moreOptions")}
>
<i className="fa fa-ellipsis-vertical"></i>
</button>
Expand Down
10 changes: 7 additions & 3 deletions frontend/app/aipanel/aipanelinput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Tooltip } from "@/element/tooltip";
import { cn } from "@/util/util";
import { useAtom, useAtomValue } from "jotai";
import { memo, useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";

interface AIPanelInputProps {
onSubmit: (e: React.FormEvent) => void;
Expand All @@ -22,6 +23,7 @@ export interface AIPanelInputRef {
}

export const AIPanelInput = memo(({ onSubmit, status, model }: AIPanelInputProps) => {
const { t } = useTranslation("ai");
const [input, setInput] = useAtom(model.inputAtom);
const isFocused = useAtomValue(model.isWaveAIFocusedAtom);
const textareaRef = useRef<HTMLTextAreaElement>(null);
Expand Down Expand Up @@ -138,15 +140,17 @@ export const AIPanelInput = memo(({ onSubmit, status, model }: AIPanelInputProps
onKeyDown={handleKeyDown}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={model.inBuilder ? "What would you like to build..." : "Ask Wave AI anything..."}
placeholder={
model.inBuilder ? t("input.placeholderBuilder") : t("input.placeholderDefault")
}
className={cn(
"w-full text-white px-2 py-2 pr-5 focus:outline-none resize-none overflow-auto",
isFocused ? "bg-accent-900/50" : "bg-gray-800"
)}
style={{ fontSize: "13px" }}
rows={2}
/>
<Tooltip content="Attach files" placement="top" divClassName="absolute bottom-6.5 right-1">
<Tooltip content={t("input.attachFiles")} placement="top" divClassName="absolute bottom-6.5 right-1">
<button
type="button"
onClick={handleUploadClick}
Expand All @@ -157,7 +161,7 @@ export const AIPanelInput = memo(({ onSubmit, status, model }: AIPanelInputProps
<i className="fa fa-paperclip text-sm"></i>
</button>
</Tooltip>
<Tooltip content="Send message (Enter)" placement="top" divClassName="absolute bottom-1.5 right-1">
<Tooltip content={t("input.sendMessage")} placement="top" divClassName="absolute bottom-1.5 right-1">
<button
type="submit"
disabled={status !== "ready" || !input.trim()}
Expand Down
13 changes: 6 additions & 7 deletions frontend/app/aipanel/byokannouncement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { useTranslation } from "react-i18next";
import { WaveAIModel } from "./waveai-model";

const BYOKAnnouncement = () => {
const { t } = useTranslation("ai");
const model = WaveAIModel.getInstance();

const handleOpenConfig = async () => {
Expand Down Expand Up @@ -40,17 +42,14 @@ const BYOKAnnouncement = () => {
<div className="flex items-start gap-3">
<i className="fa fa-key text-blue-400 text-lg mt-0.5"></i>
<div className="text-left flex-1">
<div className="text-blue-400 font-medium mb-1">New: BYOK & Local AI Support</div>
<div className="text-secondary text-sm mb-3">
Wave AI now supports bring-your-own-key (BYOK) with OpenAI, Google Gemini, Azure, and
OpenRouter, plus local models via Ollama, LM Studio, and other OpenAI-compatible providers.
</div>
<div className="text-blue-400 font-medium mb-1">{t("byok.title")}</div>
<div className="text-secondary text-sm mb-3">{t("byok.description")}</div>
<div className="flex items-center gap-3">
<button
onClick={handleOpenConfig}
className="border border-blue-400 text-blue-400 hover:bg-blue-500/10 hover:text-blue-300 px-3 py-1.5 rounded-md text-sm font-medium cursor-pointer transition-colors"
>
Configure AI Modes
{t("byok.configure")}
</button>
<a
href="https://docs.waveterm.dev/waveai-modes"
Expand All @@ -59,7 +58,7 @@ const BYOKAnnouncement = () => {
onClick={handleViewDocs}
className="text-blue-400! hover:text-blue-300! hover:underline text-sm cursor-pointer transition-colors flex items-center gap-1"
>
View Docs <i className="fa fa-external-link text-xs"></i>
{t("byok.viewDocs")} <i className="fa fa-external-link text-xs"></i>
</a>
</div>
</div>
Expand Down
33 changes: 12 additions & 21 deletions frontend/app/aipanel/telemetryrequired.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { cn } from "@/util/util";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { WaveAIModel } from "./waveai-model";

interface TelemetryRequiredMessageProps {
className?: string;
}

const TelemetryRequiredMessage = ({ className }: TelemetryRequiredMessageProps) => {
const { t } = useTranslation("ai");
const [isEnabling, setIsEnabling] = useState(false);

const handleEnableTelemetry = async () => {
Expand All @@ -34,39 +36,28 @@ const TelemetryRequiredMessage = ({ className }: TelemetryRequiredMessageProps)
<div className="max-w-md space-y-6">
<div className="space-y-4">
<i className="fa fa-sparkles text-accent text-5xl"></i>
<h2 className="text-2xl font-semibold text-foreground">Wave AI</h2>
<p className="text-secondary leading-relaxed">
Wave AI is free to use and provides integrated AI chat that can interact with your widgets,
help you with code, analyze files, and assist with your terminal workflows.
</p>
<h2 className="text-2xl font-semibold text-foreground">{t("telemetryRequired.title")}</h2>
<p className="text-secondary leading-relaxed">{t("telemetryRequired.description")}</p>
</div>

<div className="bg-blue-900/20 border border-blue-500 rounded-lg p-4">
<div className="flex items-start gap-3">
<i className="fa fa-info-circle text-blue-400 text-lg mt-0.5"></i>
<div className="text-left">
<div className="text-blue-400 font-medium mb-1">Telemetry keeps Wave AI free</div>
<div className="text-blue-400 font-medium mb-1">
{t("telemetryRequired.telemetryTitle")}
</div>
<div className="text-secondary text-sm mb-3">
<p className="mb-2">
To keep Wave AI free for everyone, we require a small amount of <i>anonymous</i>{" "}
usage data (app version, feature usage, system info).
</p>
<p className="mb-2">
This helps us block abuse by automated systems and ensure it's used by real
people like you.
</p>
<p>
We never collect your files, prompts, keystrokes, hostnames, or personally
identifying information. Wave AI is powered by OpenAI's APIs, please refer to
OpenAI's privacy policy for details on how they handle your data.
</p>
<p className="mb-2">{t("telemetryRequired.telemetryDesc1")}</p>
<p className="mb-2">{t("telemetryRequired.telemetryDesc2")}</p>
<p>{t("telemetryRequired.telemetryDesc3")}</p>
</div>
<button
onClick={handleEnableTelemetry}
disabled={isEnabling}
className="bg-accent/80 hover:bg-accent disabled:bg-accent/50 text-background px-4 py-2 rounded-lg font-medium cursor-pointer disabled:cursor-not-allowed"
>
{isEnabling ? "Enabling..." : "Enable Telemetry and Continue"}
{isEnabling ? t("telemetryRequired.enabling") : t("telemetryRequired.enableButton")}
</button>
</div>
</div>
Expand All @@ -79,7 +70,7 @@ const TelemetryRequiredMessage = ({ className }: TelemetryRequiredMessageProps)
rel="noopener noreferrer"
className="!text-secondary hover:!text-accent/80 cursor-pointer"
>
Privacy Policy
{t("telemetryRequired.privacyPolicy")}
</a>
</div>
</div>
Expand Down
Loading