Skip to content
8 changes: 7 additions & 1 deletion src/app/dashboard/[org]/[repo]/chat/ChatPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// components/ChatPage.tsx
"use client";

import React, { useMemo } from "react";
import { Chat } from "./components/Chat";
import { api } from "~/trpc/react";
import LoadingIndicator from "../components/LoadingIndicator";
import { useSearchParams } from "next/navigation";

interface ChatPageProps {
org: string;
Expand All @@ -21,6 +21,11 @@ const ChatPage: React.FC<ChatPageProps> = ({ org, repo }) => {
repo,
});

const searchParams = useSearchParams();
const selectedFilePath = searchParams.get("file_path")
? decodeURIComponent(searchParams.get("file_path")!)
: undefined;

const memoizedContextItems = useMemo(
() => contextItems ?? [],
[contextItems],
Expand All @@ -40,6 +45,7 @@ const ChatPage: React.FC<ChatPageProps> = ({ org, repo }) => {
contextItems={memoizedContextItems}
org={org}
repo={repo}
selectedFilePath={selectedFilePath}
/>
</div>
);
Expand Down
23 changes: 16 additions & 7 deletions src/app/dashboard/[org]/[repo]/chat/components/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// components/Chat.tsx
import { useState, useEffect, useRef } from "react";
import { type Message, useChat } from "ai/react";
import { type Project } from "~/server/db/tables/projects.table";
Expand All @@ -22,6 +21,7 @@ interface ChatProps {
contextItems: ContextItem[];
org: string;
repo: string;
selectedFilePath?: string;
}

export interface CodeFile {
Expand All @@ -44,7 +44,7 @@ const STARTING_MESSAGE = {
"Hi, I'm JACoB. I can answer questions about your codebase. Ask me anything!",
};

export function Chat({ contextItems, org, repo }: ChatProps) {
export function Chat({ contextItems, org, repo, selectedFilePath }: ChatProps) {
const [artifactContent, setArtifactContent] = useState<string | null>(null);
const [artifactFileName, setArtifactFileName] = useState<string>("");
const [artifactLanguage, setArtifactLanguage] = useState<string>("");
Expand Down Expand Up @@ -81,7 +81,6 @@ export function Chat({ contextItems, org, repo }: ChatProps) {
initialMessages: savedMessages,
onResponse: async (response) => {
console.log("onResponse", response);
// turn off the loading indicator
setHasStartedStreaming(true);
},
onError: (error) => {
Expand Down Expand Up @@ -115,9 +114,6 @@ export function Chat({ contextItems, org, repo }: ChatProps) {
const lastMessage = messages[messages.length - 1];
if (!lastMessage) return;
if (lastMessage.role === "assistant" && lastMessage.content.length > 0) {
// this is a workaround, but if the last message is an assistant message and there is
// more than a 1000ms gap between the last message and the onFinished call, then we know
// that the system is creating an artifact and we should show the loading card
setIsCreatingArtifact(true);
}
}, [messages]);
Expand All @@ -126,7 +122,6 @@ export function Chat({ contextItems, org, repo }: ChatProps) {

useEffect(() => {
if (model) {
// when the model changes, move the messages from the previous model to the new model
setSavedMessages(messages);
}
}, [model, messages]);
Expand Down Expand Up @@ -163,6 +158,20 @@ export function Chat({ contextItems, org, repo }: ChatProps) {
}
}, [codeContent]);

useEffect(() => {
if (selectedFilePath) {
const matchingFile = contextItems.find(
(item) => item.file === selectedFilePath,
);
if (matchingFile) {
setSelectedFiles([selectedFilePath]);
void refetchCodeContent();
} else {
toast.error("Selected file not found in the codebase context.");
}
}
}, [selectedFilePath, contextItems, refetchCodeContent]);

const handleSearchResultSelect = (filePath: string) => {
setSelectedFiles([filePath]);
void refetchCodeContent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
faChevronDown,
faCopy,
faCheck,
faComment,
} from "@fortawesome/free-solid-svg-icons";
import Mermaid from "./Mermaid";
import Markdown, { type Components } from "react-markdown";
Expand All @@ -25,6 +26,7 @@ import {
} from "react-syntax-highlighter/dist/cjs/styles/prism";
import { faClipboard } from "@fortawesome/free-solid-svg-icons";
import { toast } from "react-toastify";
import { useRouter } from "next/navigation";

interface CodebaseDetailsProps {
item: ContextItem;
Expand All @@ -42,21 +44,20 @@ const copyToClipboard = async (text: string) => {
toast.success("Copied to clipboard");
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-redundant-type-constituents
export const renderers: Partial<Components | any> = {
export const renderers: Partial<Components> = {
code: ({
inline,
className,
theme,
children,
theme,
...props
}: {
inline: boolean;
className: string;
theme: "light" | "dark";
inline?: boolean;
className?: string;
children: React.ReactNode;
theme?: "light" | "dark";
}) => {
const match = /language-(\w+)/.exec(className || "");
const match = /language-(\w+)/.exec(className ?? "");
if (!inline && match) {
return (
<div className="relative">
Expand All @@ -70,21 +71,18 @@ export const renderers: Partial<Components | any> = {
style={theme === "dark" ? oneDark : oneLight}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
</div>
);
} else if (inline) {
// Render inline code with `<code>` instead of `<div>`
return (
<code className={className} {...props}>
{children}
</code>
);
} else {
// Fallback for non-highlighted code
return (
<code className={className} {...props}>
{children}
Expand All @@ -105,6 +103,7 @@ const CodebaseDetails: React.FC<CodebaseDetailsProps> = ({
theme,
}) => {
const [copyStatus, setCopyStatus] = useState(false);
const router = useRouter();

const handleCopy = () => {
navigator.clipboard
Expand All @@ -118,8 +117,19 @@ const CodebaseDetails: React.FC<CodebaseDetailsProps> = ({
});
};

const handleSendToChat = () => {
if (item.file) {
const encodedFilePath = encodeURIComponent(item.file);
router.push(
`/dashboard/${item.file.split("/")[1]}/${item.file.split("/")[2]}/chat?file_path=${encodedFilePath}`,
);
} else {
toast.error("No file selected to send to chat.");
}
};

return (
<div className="details hide-scrollbar h-full overflow-scroll bg-white text-left text-sm text-gray-800 dark:bg-gray-900 dark:text-white">
<div className="details hide-scrollbar relative h-full overflow-scroll bg-white text-left text-sm text-gray-800 dark:bg-gray-900 dark:text-white">
<div className="sticky top-0 z-10 flex h-12 items-center justify-between bg-gradient-to-r from-aurora-50 to-aurora-100/70 px-4 shadow-sm dark:from-gray-800 dark:to-gray-700">
<div className="flex items-center space-x-3">
<button
Expand Down Expand Up @@ -151,7 +161,7 @@ const CodebaseDetails: React.FC<CodebaseDetailsProps> = ({
</div>
</div>

<div className="mt-4 space-y-6 px-4">
<div className="mt-4 space-y-6 px-4 pb-16">
<p className="mb-3 text-gray-700 dark:text-gray-100">{item.overview}</p>
{item.diagram && <Mermaid chart={item.diagram} theme={theme} />}
<Section
Expand Down Expand Up @@ -187,6 +197,16 @@ const CodebaseDetails: React.FC<CodebaseDetailsProps> = ({
<CodeSection code={item.code} theme={theme} />
) : null}
</div>

<div className="fixed bottom-0 left-0 right-0 flex justify-center bg-white p-4 dark:bg-gray-900">
<button
onClick={handleSendToChat}
className="flex items-center rounded-md bg-primary-500 px-4 py-2 text-white transition-colors hover:bg-primary-600"
>
<FontAwesomeIcon icon={faComment} className="mr-2" />
Send Filename to Chat
</button>
</div>
</div>
);
};
Expand Down