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
1 change: 1 addition & 0 deletions frontend/src/app/agent/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default function AgentChat() {

// Use optimized reducer for state management
const [agentStore, dispatchAgentStore] = useReducer(agentStoreReducer, {});
console.log("🚀 ~ AgentChat ~ agentStore:", agentStore);
// TODO: temporary conversation id (after will remove hardcoded)
const curConversationId = useRef<string>(`${agentName}_conv_default_user`);
const curThreadId = useRef<string>("");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { type FC, memo, useCallback, useState } from "react";
import ScrollContainer from "@/components/valuecell/scroll/scroll-container";
import {
MultiSectionProvider,
useMultiSection,
} from "@/provider/multi-section-provider";
import type {
AgentInfo,
ConversationView,
SectionComponentType,
} from "@/types/agent";
import ChatConversationHeader from "./chat-conversation-header";
import ChatDynamicComponent from "./chat-dynamic-component";
import ChatInputArea from "./chat-input-area";
import ChatMultiSectionComponent from "./chat-multi-section-component";
import ChatSectionComponent from "./chat-section-component";
import ChatThreadArea from "./chat-thread-area";
import ChatWelcomeScreen from "./chat-welcome-screen";

Expand All @@ -18,13 +23,14 @@ interface ChatConversationAreaProps {
sendMessage: (message: string) => Promise<void>;
}

const ChatConversationArea: FC<ChatConversationAreaProps> = ({
const ChatConversationAreaContent: FC<ChatConversationAreaProps> = ({
agent,
currentConversation,
isStreaming,
sendMessage,
}) => {
const [inputValue, setInputValue] = useState<string>("");
const { currentSection } = useMultiSection();

const handleSendMessage = useCallback(async () => {
if (!inputValue.trim()) return;
Expand Down Expand Up @@ -84,14 +90,14 @@ const ChatConversationArea: FC<ChatConversationAreaProps> = ({
/>
</section>

{/* Dynamic sections: one section per special component_type */}
{/* Chat section components: one section per special component_type */}
{currentConversation.sections &&
Object.entries(currentConversation.sections).map(
([componentType, items]) => (
<section key={componentType} className="flex flex-1 flex-col py-4">
<ScrollContainer>
{/* Section content using dynamic component rendering */}
<ChatDynamicComponent
<ChatSectionComponent
// TODO: componentType as type assertion is not safe, find a better way to do this
componentType={componentType as SectionComponentType}
items={items}
Expand All @@ -100,8 +106,28 @@ const ChatConversationArea: FC<ChatConversationAreaProps> = ({
</section>
),
)}

{/* Multi-section detail view */}
{currentSection && (
<section className="flex flex-1 flex-col py-4">
<ScrollContainer>
<ChatMultiSectionComponent
componentType={currentSection.componentType}
data={currentSection.data}
/>
</ScrollContainer>
</section>
)}
</div>
);
};

const ChatConversationArea: FC<ChatConversationAreaProps> = (props) => {
return (
<MultiSectionProvider>
<ChatConversationAreaContent {...props} />
</MultiSectionProvider>
);
};

export default memo(ChatConversationArea);
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { type FC, memo } from "react";
import BackButton from "@/components/valuecell/button/back-button";
import { MarkdownRenderer } from "@/components/valuecell/renderer";
import { useMultiSection } from "@/provider/multi-section-provider";
import type { MultiSectionComponentType } from "@/types/agent";

// define different component types and their specific rendering components
const ReportComponent: FC<{ data: string }> = ({ data }) => {
const { closeSection } = useMultiSection();

return (
<>
<BackButton className="mb-3" onClick={closeSection} />
<MarkdownRenderer content={data} />
</>
);
};

const MULTI_SECTION_COMPONENT_MAP: Record<
MultiSectionComponentType,
FC<{ data: string }>
> = {
report: ReportComponent,
};

interface ChatMultiSectionComponentProps {
componentType: MultiSectionComponentType;
data: string;
}

const ChatMultiSectionComponent: FC<ChatMultiSectionComponentProps> = ({
componentType,
data,
}) => {
const Component = MULTI_SECTION_COMPONENT_MAP[componentType];
return <Component data={data} />;
};

export default memo(ChatMultiSectionComponent);
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { parse } from "best-effort-json-parser";
import { type FC, memo, useState } from "react";
import BackButton from "@/components/valuecell/button/back-button";
import { MarkdownRenderer } from "@/components/valuecell/renderer";
Expand Down Expand Up @@ -33,9 +32,7 @@ const SecFeedComponent: FC<{ items: ChatItem[] }> = ({ items }) => {
<Component
key={item.item_id}
content={item.payload.content}
onClick={() =>
setSelectedItemContent(parse(item.payload.content).data)
}
onOpen={(data) => setSelectedItemContent(data)}
/>
),
)}
Expand All @@ -47,11 +44,14 @@ const SecFeedComponent: FC<{ items: ChatItem[] }> = ({ items }) => {
};

// component mapping table
const COMPONENT_MAP: Record<SectionComponentType, FC<{ items: ChatItem[] }>> = {
const SECTION_COMPONENT_MAP: Record<
SectionComponentType,
FC<{ items: ChatItem[] }>
> = {
sec_feed: SecFeedComponent,
};

interface ChatDynamicComponentProps {
interface ChatSectionComponentProps {
componentType: SectionComponentType;
items: ChatItem[];
}
Expand All @@ -60,13 +60,13 @@ interface ChatDynamicComponentProps {
* dynamic component renderer
* @description dynamically select the appropriate component to render based on componentType
*/
const ChatDynamicComponent: FC<ChatDynamicComponentProps> = ({
const ChatSectionComponent: FC<ChatSectionComponentProps> = ({
componentType,
items,
}) => {
const Component = COMPONENT_MAP[componentType];
const Component = SECTION_COMPONENT_MAP[componentType];

return <Component items={items} />;
};

export default memo(ChatDynamicComponent);
export default memo(ChatSectionComponent);
23 changes: 23 additions & 0 deletions frontend/src/components/valuecell/renderer/index.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.border-gradient {
background-image: linear-gradient(#f9fafb, #f9fafb);
background-clip: padding-box;
background-origin: border-box;
border: 1px solid var(--color-gray-100);
transition: border-color 0.2s ease-in-out;

&[data-active="true"] {
background-image:
linear-gradient(#f9fafb, #f9fafb),
linear-gradient(to bottom right, #ff7080, #3e88ff, #ff7080);
background-clip: padding-box, border-box;
border-color: transparent;
}

&:hover {
background-image:
linear-gradient(#f9fafb, #f9fafb),
linear-gradient(to bottom right, #ff7080, #3e88ff, #ff7080);
background-clip: padding-box, border-box;
border-color: transparent;
}
}
3 changes: 2 additions & 1 deletion frontend/src/components/valuecell/renderer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as MarkdownRenderer } from "./markdown-renderer";
export { default as SecFeedRenderer } from "./sec-feed-renderer";
export { default as ReportRenderer } from "./report-renderer";
export { default as SecFeedRenderer } from "./sec-feed-renderer";
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { type FC, memo } from "react";
import ReactMarkdown from "react-markdown";

interface MarkdownRendererProps {
content: string;
}
import type { MarkdownRendererProps } from "@/types/renderer";

const MarkdownRenderer: FC<MarkdownRendererProps> = ({ content }) => {
return (
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/components/valuecell/renderer/report-renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { parse } from "best-effort-json-parser";
import { ChevronRight, FileText } from "lucide-react";
import { type FC, memo } from "react";
import { TIME_FORMATS, TimeUtils } from "@/lib/time";
import { cn } from "@/lib/utils";
import { useMultiSection } from "@/provider/multi-section-provider";
import type { ReportRendererProps } from "@/types/renderer";
import styles from "./index.module.css";

const ReportRenderer: FC<ReportRendererProps> = ({ content, isActive }) => {
const { title, create_time, data } = parse(content);
const { openSection } = useMultiSection();

return (
<div
data-active={isActive}
className={cn(
"flex h-full min-w-96 items-center justify-between gap-2 rounded-xl px-4 py-5",
"cursor-pointer transition-all duration-200",
styles["border-gradient"],
)}
onClick={() => openSection("report", data)}
>
{/* Left side: Icon and text */}
<div className="flex items-center gap-2">
{/* Document icon with background */}
<div className="flex size-10 items-center justify-center rounded-xl bg-gradient-to-br from-5% from-[#3A88FF] to-80% to-[#FF6699]">
<FileText className="size-6 text-white" />
</div>

{/* Text content */}
<div className="flex flex-col gap-1">
<p className="font-normal text-base text-gray-950 leading-5">
{title}
</p>
<p className="whitespace-nowrap text-gray-400 text-xs leading-4">
{`Created at: ${TimeUtils.fromUTC(create_time).format(TIME_FORMATS.DATE)}`}
</p>
</div>
</div>

{/* Right side: Arrow icon */}
<ChevronRight className="size-6 text-gray-700" />
</div>
);
};

export default memo(ReportRenderer);
22 changes: 9 additions & 13 deletions frontend/src/components/valuecell/renderer/sec-feed-renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import { parse } from "best-effort-json-parser";
import { type FC, memo } from "react";
import { TIME_FORMATS, TimeUtils } from "@/lib/time";
import { cn } from "@/lib/utils";
import type { SecFeedRendererProps } from "@/types/renderer";
import styles from "./index.module.css";
import MarkdownRenderer from "./markdown-renderer";

interface SecFeedRendererProps {
content: string;
onClick?: () => void;
}

const SecFeedRenderer: FC<SecFeedRendererProps> = ({ content, onClick }) => {
const SecFeedRenderer: FC<SecFeedRendererProps> = ({ content, onOpen }) => {
const { ticker, data, source, create_time } = parse(content);

return (
<div
className="group relative flex h-full cursor-pointer flex-col gap-3 rounded-2xl bg-gray-50 p-4 transition-all duration-200 hover:shadow-sm"
onClick={onClick}
className={cn(
"group relative flex h-full cursor-pointer flex-col gap-3 rounded-2xl bg-gray-50 p-4 transition-all",
styles["border-gradient"],
)}
onClick={() => onOpen?.(data)}
>
{/* gradient border on hover */}
<div className="pointer-events-none absolute inset-0 rounded-2xl bg-gradient-to-br from-red-400 via-pink-300 to-pink-200 p-px opacity-0 transition-opacity duration-200 group-hover:opacity-100">
<div className="h-full rounded-2xl bg-gray-50" />
</div>

{/* content */}
<div className="relative z-10 max-h-24 w-full overflow-hidden">
<MarkdownRenderer content={data} />
Expand Down
18 changes: 13 additions & 5 deletions frontend/src/constants/agent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { FC } from "react";
import {
AswathDamodaranPng,
BenGrahamPng,
Expand All @@ -21,25 +20,34 @@ import {
} from "@/assets/png";
import {
MarkdownRenderer,
ReportRenderer,
SecFeedRenderer,
} from "@/components/valuecell/renderer";
import type { AgentComponentType } from "@/types/agent";
import type { RendererComponent } from "@/types/renderer";

// component_type to section type
export const AGENT_SECTION_COMPONENT_TYPE = ["sec_feed"] as const;

// multi section component type
export const AGENT_MULTI_SECTION_COMPONENT_TYPE = ["report"] as const;

// agent component type
export const AGENT_COMPONENT_TYPE = [
"markdown",
"tool_call",
...AGENT_SECTION_COMPONENT_TYPE,
...AGENT_MULTI_SECTION_COMPONENT_TYPE,
] as const;

export const COMPONENT_RENDERER_MAP: Record<
AgentComponentType,
FC<{ content: string; onClick?: () => void }>
> = {
/**
* Component renderer mapping with automatic type inference
*/
export const COMPONENT_RENDERER_MAP: {
[K in AgentComponentType]: RendererComponent<K>;
} = {
sec_feed: SecFeedRenderer,
report: ReportRenderer,
markdown: MarkdownRenderer,
tool_call: MarkdownRenderer,
};
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ dayjs.extend(relativeTime);
* Common time format constants
*/
export const TIME_FORMATS = {
DATE: "YYYY-MM-DD",
DATE: "YYYY/MM/DD",
TIME: "HH:mm:ss",
DATETIME: "YYYY-MM-DD HH:mm:ss",
DATETIME_SHORT: "YYYY-MM-DD HH:mm",
Expand Down
Loading