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
310 changes: 253 additions & 57 deletions frontend/bun.lock

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-router/node": "^7.8.2",
"@tanstack/react-query": "^5.89.0",
"@tauri-apps/api": "^2.8.0",
"@tauri-apps/plugin-opener": "^2.5.0",
"class-variance-authority": "^0.7.1",
Expand All @@ -28,23 +29,28 @@
"overlayscrollbars-react": "^0.5.6",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-markdown": "^10.1.0",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@biomejs/biome": "^2.2.4",
"@react-router/dev": "^7.8.2",
"@react-router/serve": "^7.8.2",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.13",
"@tauri-apps/cli": "^2.8.4",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"shadcn": "^3.2.1",
"tailwindcss": "^4.1.13",
"tw-animate-css": "^1.3.8",
"typescript": "~5.9.2",
"typescript": "^5.9.2",
"vite": "^7.0.4",
"vite-plugin-svg-sprite": "^0.6.3",
"vite-tsconfig-paths": "^5.1.4"
},
"packageManager": "bun@1.2.21"
"overrides": {
"vite": "npm:rolldown-vite@latest"
},
"packageManager": "bun@1.2.22"
}
29 changes: 29 additions & 0 deletions frontend/src/api/stock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { API_QUERY_KEYS, USER_LANGUAGE } from "@/constants/api";
import { type ApiResponse, apiClient } from "@/lib/api-client";
import type { Stock } from "@/types/stock";

export const useGetWatchlist = () =>
useQuery({
queryKey: API_QUERY_KEYS.STOCK.watchlist,
queryFn: (): Promise<Stock[]> => apiClient.get<Stock[]>("watchlist"),
});

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

export const useAddStockToWatchlist = () => {
return useMutation({
mutationFn: (ticker: Pick<Stock, "ticker">) =>
apiClient.post<ApiResponse<null>>("watchlist/stocks", ticker),
});
};
23 changes: 23 additions & 0 deletions frontend/src/app/agent/chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useParams } from "react-router";

export default function AgentChat() {
const { agentId } = useParams();

return (
<div className="flex min-h-screen items-center justify-center bg-[#eef0f3] p-4">
<div className="w-full max-w-[1360px] rounded-[12px] bg-white shadow-lg">
<div className="flex h-full flex-col p-8">
<h1 className="font-medium text-3xl text-black leading-9">
Chat with Agent: {agentId}
</h1>
<div className="mt-8 flex-1">
{/* Chat interface will be implemented here */}
<div className="flex h-full items-center justify-center text-gray-500">
Chat interface coming soon...
</div>
</div>
</div>
</div>
</div>
);
}
43 changes: 43 additions & 0 deletions frontend/src/app/agent/config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ArrowRight } from "lucide-react";
import { Link, useParams } from "react-router";
import BackButton from "@/components/valuecell/button/back-button";
import PreviewMarkdown from "@/components/valuecell/markdown/preview-markdown";
import ScrollContainer from "@/components/valuecell/scroll-container";
import { agentData } from "@/mock/agent-data";

export default function AgentConfig() {
const { agentId } = useParams();

const agent = agentData[agentId as keyof typeof agentData];

return (
<div className="flex flex-1 flex-col gap-8 overflow-hidden py-8">
<BackButton className="mx-8" />

{/* Agent info and configure button */}
<div className="mb-10 flex items-start justify-between px-8">
<div className="flex flex-col gap-4">
{agent.avatar}
<div className="flex flex-col gap-2">
<h1 className="font-semibold text-4xl leading-9">{agent.name}</h1>
<p className="text-base text-neutral-500 leading-6">
{agent.description}
</p>
</div>
</div>

<Link
className="flex items-center gap-2 rounded-md bg-black px-5 py-3 font-semibold text-base text-white hover:bg-black/80"
to={`/agent/${agentId}`}
>
Activate Chat
<ArrowRight size={16} />
</Link>
</div>

<ScrollContainer className="px-8">
<PreviewMarkdown content={agent.content} />
</ScrollContainer>
</div>
);
}
47 changes: 10 additions & 37 deletions frontend/src/app/home/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,26 @@
import { Plus } from "lucide-react";
import { Outlet, useLocation } from "react-router";
import { Outlet } from "react-router";
import StockSearchModal from "@/app/home/components/stock-search-modal";
import { Button } from "@/components/ui/button";
import {
StockMenu,
StockMenuGroup,
StockMenuGroupHeader,
StockMenuHeader,
StockMenuListItem,
} from "@/components/valuecell/menus/stock-menus";
import ScrollContainer from "@/components/valuecell/scroll-container";
import { stockData } from "@/mock/stock-data";
import StockList from "./components/stock-list";

export default function HomeLayout() {
const { pathname } = useLocation();

// Extract stock symbol (e.g., AAPL) from path like /stock/AAPL
const stockSymbol = pathname.split("/")[2];

return (
<div className="flex flex-1 overflow-hidden">
<ScrollContainer className="flex-1">
<Outlet />
</ScrollContainer>

<aside className="flex h-full flex-col justify-between border-l">
<StockMenu>
<StockMenuHeader>My Stocks</StockMenuHeader>
<ScrollContainer>
{stockData.map((group) => (
<StockMenuGroup key={group.title}>
<StockMenuGroupHeader>{group.title}</StockMenuGroupHeader>
{group.stocks.map((stock) => (
<StockMenuListItem
key={stock.symbol}
stock={stock}
to={`/stock/${stock.symbol}`}
isActive={stockSymbol === stock.symbol}
replace={!!stockSymbol}
/>
))}
</StockMenuGroup>
))}
</ScrollContainer>
</StockMenu>
<StockList />

<Button variant="secondary" className="mx-5 mb-6 font-bold text-sm">
<Plus size={16} />
Add Stocks
</Button>
<StockSearchModal>
<Button variant="secondary" className="mx-5 mb-6 font-bold text-sm">
<Plus size={16} />
Add Stocks
</Button>
</StockSearchModal>
</aside>
</div>
);
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/app/home/components/stock-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { memo } from "react";
import { useLocation } from "react-router";
import {
StockMenu,
StockMenuGroup,
StockMenuGroupHeader,
StockMenuHeader,
StockMenuListItem,
} from "@/components/valuecell/menus/stock-menus";
import ScrollContainer from "@/components/valuecell/scroll-container";
import { stockData } from "@/mock/stock-data";

function StockList() {
const { pathname } = useLocation();

// Extract stock symbol (e.g., AAPL) from path like /stock/AAPL
const stockSymbol = pathname.split("/")[2];

return (
<StockMenu>
<StockMenuHeader>My Stocks</StockMenuHeader>
<ScrollContainer>
{stockData.map((group) => (
<StockMenuGroup key={group.title}>
<StockMenuGroupHeader>{group.title}</StockMenuGroupHeader>
{group.stocks.map((stock) => (
<StockMenuListItem
key={stock.symbol}
stock={stock}
to={`/stock/${stock.symbol}`}
isActive={stockSymbol === stock.symbol}
replace={!!stockSymbol}
/>
))}
</StockMenuGroup>
))}
</ScrollContainer>
</StockMenu>
);
}

export default memo(StockList);
104 changes: 104 additions & 0 deletions frontend/src/app/home/components/stock-search-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Plus, Search, X } from "lucide-react";
import { useState } from "react";
import { useAddStockToWatchlist, useGetStocksList } from "@/api/stock";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import ScrollContainer from "@/components/valuecell/scroll-container";
import { useDebounce } from "@/hooks/use-debounce";

interface StockSearchModalProps {
children: React.ReactNode;
}

export default function StockSearchModal({ children }: StockSearchModalProps) {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
const { data: stockList, isLoading } = useGetStocksList({
query: debouncedQuery,
});

const { mutate: addStockToWatchlist } = useAddStockToWatchlist();

return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
className="flex h-3/5 min-h-[400px] w-md flex-col gap-3 rounded-2xl bg-neutral-50 p-6"
showCloseButton={false}
>
<header className="flex items-center justify-between">
<DialogTitle className="font-semibold text-2xl text-neutral-900">
Stock Search
</DialogTitle>
<DialogClose asChild>
<Button size="icon" variant="ghost" className="cursor-pointer">
<X className="size-6 text-neutral-400" />
</Button>
</DialogClose>
</header>

{/* Search Input */}
<div className="flex items-center gap-4 rounded-lg bg-white p-4 focus-within:ring-1">
<Search className="size-5 text-neutral-400" />
<Input
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for stock name or code"
className="border-none bg-transparent p-0 text-neutral-900 text-sm shadow-none placeholder:text-neutral-400 focus-visible:ring-0 focus-visible:ring-offset-0"
/>
</div>

{/* Search Results */}
<ScrollContainer>
{isLoading ? (
<p className="p-4 text-center text-neutral-400 text-sm">
Searching...
</p>
) : stockList && stockList.length > 0 ? (
<div className="rounded-lg bg-white py-2">
{stockList.map((stock) => (
<div
key={stock.ticker}
className="flex items-center justify-between px-4 py-2 transition-colors hover:bg-gray-50"
>
<div className="flex flex-col gap-px">
<p className="text-neutral-900 text-sm">
{stock.display_name}
</p>
<p className="text-neutral-400 text-xs">{stock.ticker}</p>
</div>

<Button
size="sm"
className="cursor-pointer font-normal text-sm text-white"
onClick={() =>
addStockToWatchlist({ ticker: stock.ticker })
}
>
<Plus className="size-5" />
Watchlist
</Button>
</div>
))}
</div>
) : (
query &&
!isLoading &&
stockList &&
stockList.length === 0 && (
<p className="p-4 text-center text-neutral-400 text-sm">
No related stocks found
</p>
)
)}
</ScrollContainer>
</DialogContent>
</Dialog>
);
}
Loading