diff --git a/kura/types/conversation.py b/kura/types/conversation.py index e3cb25f..8c0181e 100644 --- a/kura/types/conversation.py +++ b/kura/types/conversation.py @@ -24,7 +24,9 @@ def from_claude_conversation_dump(cls, file_path: str) -> list["Conversation"]: created_at=conversation["created_at"], messages=[ Message( - created_at=message["created_at"], + created_at=datetime.fromisoformat( + message["created_at"].replace("Z", "+00:00") + ), role="user" if message["sender"] == "human" else "assistant", @@ -36,7 +38,15 @@ def from_claude_conversation_dump(cls, file_path: str) -> list["Conversation"]: ] ), ) - for message in conversation["chat_messages"] + for message in sorted( + conversation["chat_messages"], + key=lambda x: ( + datetime.fromisoformat( + x["created_at"].replace("Z", "+00:00") + ), + 0 if x["sender"] == "human" else 1, + ), + ) ], ) for conversation in json.load(f) diff --git a/ui/bun.lockb b/ui/bun.lockb index c66b3c9..029987e 100755 Binary files a/ui/bun.lockb and b/ui/bun.lockb differ diff --git a/ui/src/components/ChatDialog.tsx b/ui/src/components/ChatDialog.tsx index 4bb81af..752df48 100644 --- a/ui/src/components/ChatDialog.tsx +++ b/ui/src/components/ChatDialog.tsx @@ -1,5 +1,5 @@ import { Conversation } from "@/types/conversation"; -import React from "react"; + import { Dialog, DialogContent, @@ -8,6 +8,7 @@ import { DialogTitle, DialogTrigger, } from "./ui/dialog"; +import { Markdown } from "./markdown"; type Props = { chatId: string; @@ -36,13 +37,13 @@ const ChatDialog = ({ chatId, conversation }: Props) => { }`} >
-

{m.content}

+ {m.content}
{m.role === "user" ? "You" : "Assistant"} diff --git a/ui/src/components/code-block.tsx b/ui/src/components/code-block.tsx new file mode 100644 index 0000000..7bf5b8f --- /dev/null +++ b/ui/src/components/code-block.tsx @@ -0,0 +1,38 @@ +"use client"; + +interface CodeBlockProps { + node: any; + inline: boolean; + className: string; + children: any; +} + +export function CodeBlock({ + node, + inline, + className, + children, + ...props +}: CodeBlockProps) { + if (!inline) { + return ( +
+
+          {children}
+        
+
+ ); + } else { + return ( + + {children} + + ); + } +} diff --git a/ui/src/components/markdown.tsx b/ui/src/components/markdown.tsx new file mode 100644 index 0000000..ff3996f --- /dev/null +++ b/ui/src/components/markdown.tsx @@ -0,0 +1,107 @@ +import { memo } from "react"; +import ReactMarkdown, { type Components } from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { CodeBlock } from "./code-block"; + +const components: Partial = { + // @ts-expect-error + code: CodeBlock, + pre: ({ children }) => <>{children}, + ol: ({ node, children, ...props }) => { + return ( +
    + {children} +
+ ); + }, + li: ({ node, children, ...props }) => { + return ( +
  • + {children} +
  • + ); + }, + ul: ({ node, children, ...props }) => { + return ( + + ); + }, + strong: ({ node, children, ...props }) => { + return ( + + {children} + + ); + }, + a: ({ node, children, ...props }) => { + return ( + + {children} + + ); + }, + h1: ({ node, children, ...props }) => { + return ( +

    + {children} +

    + ); + }, + h2: ({ node, children, ...props }) => { + return ( +

    + {children} +

    + ); + }, + h3: ({ node, children, ...props }) => { + return ( +

    + {children} +

    + ); + }, + h4: ({ node, children, ...props }) => { + return ( +

    + {children} +

    + ); + }, + h5: ({ node, children, ...props }) => { + return ( +
    + {children} +
    + ); + }, + h6: ({ node, children, ...props }) => { + return ( +
    + {children} +
    + ); + }, +}; + +const remarkPlugins = [remarkGfm]; + +const NonMemoizedMarkdown = ({ children }: { children: string }) => { + return ( + + {children} + + ); +}; + +export const Markdown = memo( + NonMemoizedMarkdown, + (prevProps, nextProps) => prevProps.children === nextProps.children +);