Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ API_I18N_ENABLED=true
API_HOST=localhost
API_PORT=8000

# Supported languages: en_US, en_GB, zh-Hans, zh-Hant
# You can interact with the agent in your preferred language.
# Supported languages: en, zh_CN, zh_TW, ja
# You can interact with the agent in your preferred language.
# This variable is mainly intended for frontend use; setting LANG is not required.
LANG=en-US
LANG=en
TIMEZONE=America/New_York
PYTHONIOENCODING=utf-8

Expand Down
12 changes: 12 additions & 0 deletions frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@
"dayjs": "^1.11.19",
"echarts": "^6.0.0",
"framer-motion": "^12.23.26",
"i18next": "^25.7.2",
"isbot": "5.1.31",
"lucide-react": "^0.559.0",
"mutative": "^1.3.0",
"next-themes": "^0.4.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-i18next": "^16.5.0",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/api/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,25 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { VALUECELL_AGENT } from "@/constants/agent";
import { API_QUERY_KEYS } from "@/constants/api";
import { type ApiResponse, apiClient } from "@/lib/api-client";
import { useLanguage } from "@/store/settings-store";
import type { AgentInfo } from "@/types/agent";

export const useGetAgentInfo = (params: { agentName: string }) => {
const language = useLanguage();

return useQuery({
queryKey: API_QUERY_KEYS.AGENT.agentInfo(Object.values(params)),
queryKey: API_QUERY_KEYS.AGENT.agentInfo([
...Object.values(params),
language,
]),
queryFn: async () => {
// Return hardcoded data for ValueCellAgent
if (params.agentName === "ValueCellAgent") {
return Promise.resolve({ data: VALUECELL_AGENT });
}
// Fetch from API for other agents
return apiClient.get<ApiResponse<AgentInfo>>(
`/agents/by-name/${params.agentName}`,
`/agents/by-name/${params.agentName}?language=${language}`,
);
},
select: (data) => data.data,
Expand All @@ -24,11 +30,16 @@ export const useGetAgentInfo = (params: { agentName: string }) => {
export const useGetAgentList = (
params: { enabled_only: string } = { enabled_only: "false" },
) => {
const language = useLanguage();

return useQuery({
queryKey: API_QUERY_KEYS.AGENT.agentList(Object.values(params)),
queryKey: API_QUERY_KEYS.AGENT.agentList([
...Object.values(params),
language,
]),
queryFn: () =>
apiClient.get<ApiResponse<{ agents: AgentInfo[] }>>(
`/agents/?enabled_only=${params.enabled_only}`,
`/agents/?enabled_only=${params.enabled_only}&language=${language}`,
),
select: (data) => data.data.agents,
});
Expand Down
18 changes: 9 additions & 9 deletions frontend/src/api/stock.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
API_QUERY_KEYS,
USER_LANGUAGE,
VALUECELL_BACKEND_URL,
} from "@/constants/api";
import { API_QUERY_KEYS, VALUECELL_BACKEND_URL } from "@/constants/api";
import { type ApiResponse, apiClient } from "@/lib/api-client";
import { useLanguage } from "@/store/settings-store";
import { useSystemStore } from "@/store/system-store";
import type {
Stock,
Expand All @@ -22,17 +19,20 @@ export const useGetWatchlist = () =>
select: (data) => data.data,
});

export const useGetStocksList = (params: { query: string }) =>
useQuery({
queryKey: API_QUERY_KEYS.STOCK.stockSearch(Object.values(params)),
export const useGetStocksList = (params: { query: string }) => {
const language = useLanguage();

return useQuery({
queryKey: API_QUERY_KEYS.STOCK.stockSearch([params.query, language]),
queryFn: ({ signal }) =>
apiClient.get<ApiResponse<{ results: Stock[] }>>(
`watchlist/asset/search?q=${params.query}&language=${USER_LANGUAGE}`,
`watchlist/asset/search?q=${params.query}&language=${language}`,
{ signal },
),
select: (data) => data.data.results,
enabled: !!params.query,
});
};

export const useAddStockToWatchlist = () => {
const queryClient = useQueryClient();
Expand Down
32 changes: 23 additions & 9 deletions frontend/src/api/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { API_QUERY_KEYS, VALUECELL_BACKEND_URL } from "@/constants/api";
import { type ApiResponse, apiClient } from "@/lib/api-client";
import { useLanguage } from "@/store/settings-store";
import { useSystemStore } from "@/store/system-store";
import type {
StrategyDetail,
Expand Down Expand Up @@ -68,22 +69,29 @@ export const useSignOut = () => {
export const useGetStrategyList = (
params: { limit: number; days: number } = { limit: 10, days: 7 },
) => {
const language = useLanguage();

return useQuery({
queryKey: API_QUERY_KEYS.SYSTEM.strategyList(Object.values(params)),
queryKey: API_QUERY_KEYS.SYSTEM.strategyList([
...Object.values(params),
language,
]),
queryFn: () =>
apiClient.get<ApiResponse<StrategyRankItem[]>>(
`${VALUECELL_BACKEND_URL}/strategy/list?limit=${params.limit}&days=${params.days}`,
`${VALUECELL_BACKEND_URL}/strategy/list?limit=${params.limit}&days=${params.days}&language=${language}`,
),
select: (data) => data.data,
});
};

export const useGetStrategyDetail = (id: number | null) => {
const language = useLanguage();

return useQuery({
queryKey: API_QUERY_KEYS.SYSTEM.strategyDetail([id ?? ""]),
queryKey: API_QUERY_KEYS.SYSTEM.strategyDetail([id ?? "", language]),
queryFn: () =>
apiClient.get<ApiResponse<StrategyDetail>>(
`${VALUECELL_BACKEND_URL}/strategy/detail/${id}`,
`${VALUECELL_BACKEND_URL}/strategy/detail/${id}?language=${language}`,
),
select: (data) => data.data,
enabled: !!id,
Expand Down Expand Up @@ -122,15 +130,21 @@ export const usePublishStrategy = () => {
* @param region - Optional region override for testing (e.g., "cn" or "default").
* In development, you can set this to test different regions.
*/
export const useGetDefaultTickers = (region?: string) =>
useQuery({
queryKey: ["system", "default-tickers", region],
export const useGetDefaultTickers = (region?: string) => {
const language = useLanguage();

return useQuery({
queryKey: ["system", "default-tickers", region, language],
queryFn: () => {
const params = region ? `?region=${region}` : "";
const regionParam = region ? `region=${region}` : "";
const langParam = `language=${language}`;
const params = [regionParam, langParam].filter(Boolean).join("&");

return apiClient.get<ApiResponse<DefaultTickersResponse>>(
`system/default-tickers${params}`,
`system/default-tickers?${params}`,
);
},
select: (data) => data.data,
staleTime: 1000 * 60 * 60, // Cache for 1 hour, region doesn't change frequently
});
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQueryClient } from "@tanstack/react-query";
import { type FC, memo, useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Navigate,
useLocation,
Expand Down Expand Up @@ -42,6 +43,7 @@ interface CommonAgentAreaProps {
}

const CommonAgentAreaContent: FC<CommonAgentAreaProps> = ({ agentName }) => {
const { t } = useTranslation();
const { data: agent, isLoading: isLoadingAgent } = useGetAgentInfo({
agentName: agentName ?? "",
});
Expand Down Expand Up @@ -208,7 +210,7 @@ const CommonAgentAreaContent: FC<CommonAgentAreaProps> = ({ agentName }) => {
<>
<ChatConversationHeader agent={agent} />
<ChatWelcomeScreen
title={`Welcome to ${agent.display_name}!`}
title={t("agent.welcome", { name: agent.display_name })}
inputValue={inputValue}
onInputChange={handleInputChange}
onSendMessage={handleSendMessage}
Expand All @@ -235,7 +237,7 @@ const CommonAgentAreaContent: FC<CommonAgentAreaProps> = ({ agentName }) => {
value={inputValue}
onChange={handleInputChange}
onSend={handleSendMessage}
placeholder="Type your message..."
placeholder={t("chat.input.placeholder")}
disabled={isStreaming}
variant="chat"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Plus } from "lucide-react";
import { type FC, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
useDeleteStrategy,
useGetStrategyDetails,
Expand Down Expand Up @@ -34,6 +35,7 @@ const EmptyIllustration = () => (
);

const StrategyAgentArea: FC<AgentViewProps> = () => {
const { t } = useTranslation();
const { data: strategies = [], isLoading: isLoadingStrategies } =
useGetStrategyList();
const [selectedStrategy, setSelectedStrategy] = useState<Strategy | null>(
Expand Down Expand Up @@ -80,7 +82,7 @@ const StrategyAgentArea: FC<AgentViewProps> = () => {
<div className="flex flex-1 overflow-hidden">
{/* Left section: Strategy list */}
<div className="flex w-96 flex-col gap-4 border-r py-6 *:px-6">
<p className="font-semibold text-base">Trading Strategies</p>
<p className="font-semibold text-base">{t("strategy.title")}</p>

{strategies && strategies.length > 0 ? (
<TradeStrategyGroup
Expand All @@ -97,10 +99,9 @@ const StrategyAgentArea: FC<AgentViewProps> = () => {
) : (
<div className="flex flex-1 flex-col items-center justify-center gap-4">
<EmptyIllustration />

<div className="flex flex-col gap-3 text-center text-base text-gray-400">
<p>No trading strategies</p>
<p>Create your first trading strategy</p>
<p>{t("strategy.noStrategies")}</p>
<p>{t("strategy.createFirst")}</p>
</div>

<CreateStrategyModal>
Expand All @@ -109,7 +110,7 @@ const StrategyAgentArea: FC<AgentViewProps> = () => {
className="w-full gap-3 rounded-lg py-4 text-base"
>
<Plus className="size-6" />
Add trading strategy
{t("strategy.add")}
</Button>
</CreateStrategyModal>
</div>
Expand All @@ -135,7 +136,7 @@ const StrategyAgentArea: FC<AgentViewProps> = () => {
<div className="flex size-full flex-col items-center justify-center gap-8">
<EmptyIllustration />
<p className="font-normal text-base text-gray-400">
No running strategies
{t("strategy.noStrategies")}
</p>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MessageCircle, Settings } from "lucide-react";
import { type FC, memo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { Button } from "@/components/ui/button";
import {
Expand All @@ -16,6 +17,7 @@ interface ChatConversationHeaderProps {
}

const ChatConversationHeader: FC<ChatConversationHeaderProps> = ({ agent }) => {
const { t } = useTranslation();
return (
<header className="flex w-full items-center justify-between p-6">
<div className="flex items-center gap-2">
Expand Down Expand Up @@ -44,7 +46,7 @@ const ChatConversationHeader: FC<ChatConversationHeaderProps> = ({ agent }) => {
<MessageCircle size={16} className="text-gray-700" />
</Button>
</TooltipTrigger>
<TooltipContent>New Conversation</TooltipContent>
<TooltipContent>{t("chat.newConversation")}</TooltipContent>
</Tooltip>
</Link>
<Link to="./config">
Expand All @@ -58,7 +60,7 @@ const ChatConversationHeader: FC<ChatConversationHeaderProps> = ({ agent }) => {
<Settings size={16} className="text-gray-700" />
</Button>
</TooltipTrigger>
<TooltipContent>Settings</TooltipContent>
<TooltipContent>{t("chat.settings")}</TooltipContent>
</Tooltip>
</Link>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ArrowUp } from "lucide-react";
import { type FC, memo } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import ScrollTextarea from "@/components/valuecell/scroll/scroll-textarea";
import { cn } from "@/lib/utils";
Expand All @@ -20,11 +21,14 @@ const ChatInputArea: FC<ChatInputAreaProps> = ({
onChange,
onSend,
onKeyDown,
placeholder = "Type your message...",
placeholder,
disabled = false,
className,
variant = "chat",
}) => {
const { t } = useTranslation();
const resolvedPlaceholder = placeholder ?? t("chat.input.placeholder");

const handleKeyDown = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Send message on Enter key (excluding Shift+Enter line breaks and IME composition state)
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
Expand Down Expand Up @@ -56,7 +60,7 @@ const ChatInputArea: FC<ChatInputAreaProps> = ({
value={value}
onInput={(e) => onChange(e.currentTarget.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
placeholder={resolvedPlaceholder}
maxHeight={120}
minHeight={24}
disabled={disabled}
Expand Down
Loading