Skip to content

Commit

Permalink
Update UI (#9)
Browse files Browse the repository at this point in the history
Updated the UI and added a new `kura` conversation type for easier interop with other message types
  • Loading branch information
ivanleomk authored Jan 19, 2025
1 parent 60a797a commit 6caaf7c
Show file tree
Hide file tree
Showing 24 changed files with 1,107 additions and 198 deletions.
76 changes: 27 additions & 49 deletions kura/cli/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
from pydantic import BaseModel
from pathlib import Path
from kura import Kura
from kura.types import ProjectedCluster, Conversation, Message
from kura.types import ProjectedCluster, Conversation
from typing import Optional
from kura.cli.visualisation import (
generate_cumulative_chart_data,
generate_messages_per_chat_data,
generate_messages_per_week_data,
generate_new_chats_per_week_data,
)
import json
import os


api = FastAPI()

Expand All @@ -31,62 +32,39 @@


class ConversationData(BaseModel):
data: list[dict]
data: list[Conversation]
max_clusters: Optional[int]
disable_checkpoints: bool


@api.post("/api/analyse")
async def analyse_conversations(conversation_data: ConversationData):
conversations = [
Conversation(
chat_id=conversation["uuid"],
created_at=conversation["created_at"],
messages=[
Message(
created_at=message["created_at"],
role=message["sender"],
content="\n".join(
[
item["text"]
for item in message["content"]
if item["type"] == "text"
]
),
)
for message in conversation["chat_messages"]
],
)
for conversation in conversation_data.data
]

clusters_file = (
Path(os.path.abspath(os.environ["KURA_CHECKPOINT_DIR"]))
/ "dimensionality_checkpoints.json"
)
clusters = []

print(conversation_data.disable_checkpoints)
# Load clusters from checkpoint file if it exists

if not clusters_file.exists():
clusters_file = Path("./checkpoints") / "dimensionality.jsonl"
if not clusters_file.exists() or conversation_data.disable_checkpoints:
kura = Kura(
checkpoint_dir=str(
Path(os.path.abspath(os.environ["KURA_CHECKPOINT_DIR"]))
),
checkpoint_dir=str(clusters_file.parent),
max_clusters=conversation_data.max_clusters
if conversation_data.max_clusters
else 10,
disable_checkpoints=conversation_data.disable_checkpoints,
)
clusters = await kura.cluster_conversations(conversations)

with open(clusters_file) as f:
clusters_data = []
for line in f:
clusters_data.append(line)
clusters = [
ProjectedCluster(**json.loads(cluster)) for cluster in clusters_data
]
clusters = await kura.cluster_conversations(conversation_data.data)
else:
with open(clusters_file) as f:
clusters_data = []
for line in f:
clusters_data.append(line)
clusters = [
ProjectedCluster(**json.loads(cluster)) for cluster in clusters_data
]

return {
"cumulative_words": generate_cumulative_chart_data(conversations),
"messages_per_chat": generate_messages_per_chat_data(conversations),
"messages_per_week": generate_messages_per_week_data(conversations),
"new_chats_per_week": generate_new_chats_per_week_data(conversations),
"cumulative_words": generate_cumulative_chart_data(conversation_data.data),
"messages_per_chat": generate_messages_per_chat_data(conversation_data.data),
"messages_per_week": generate_messages_per_week_data(conversation_data.data),
"new_chats_per_week": generate_new_chats_per_week_data(conversation_data.data),
"clusters": clusters,
}

Expand Down
2 changes: 1 addition & 1 deletion kura/cli/visualisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def generate_cumulative_chart_data(conversations: List[Conversation]) -> dict:
messages_data = []
for conv in conversations:
for msg in conv.messages:
if msg.role == "human":
if msg.role == "user":
messages_data.append(
{
"datetime": pd.to_datetime(
Expand Down
4 changes: 4 additions & 0 deletions kura/kura.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
BaseMetaClusterModel,
BaseDimensionalityReduction,
)
from pathlib import Path
from typing import Union
import os
from typing import TypeVar
Expand Down Expand Up @@ -66,6 +67,9 @@ def __init__(
if not os.path.exists(self.checkpoint_dir) and not self.disable_checkpoints:
os.makedirs(self.checkpoint_dir)

if not self.disable_checkpoints:
print(f"Checkpoint directory: {Path(self.checkpoint_dir)}")

def load_checkpoint(
self, checkpoint_path: str, response_model: type[T]
) -> Union[list[T], None]:
Expand Down
14 changes: 12 additions & 2 deletions kura/types/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)
Expand Down
Binary file modified ui/bun.lockb
Binary file not shown.
5 changes: 5 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2",
Expand Down
21 changes: 18 additions & 3 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { useState } from "react";
import { UploadButton } from "./components/upload-button";
import { UploadForm } from "./components/upload-form";
import type { Analytics } from "./types/analytics";
import { AnalyticsCharts } from "./components/analytics-charts";
import ClusterVisualisation from "./components/cluster";
import { Conversation } from "./types/conversation";

function App() {
const [data, setData] = useState<Analytics | null>(null);
const [allConversations, setAllConversations] = useState<Conversation[]>([]);

const handleUploadSuccess = (analyticsData: Analytics) => {
setData(analyticsData);
};

const resetData = () => {
setData(null);
};

return (
<>
<div className="max-w-7xl mx-auto mb-10">
Expand All @@ -21,12 +27,21 @@ function App() {
Detailed metrics and insights about chat activity
</p>
</div>
<UploadButton onSuccess={handleUploadSuccess} />
<UploadForm
onSuccess={handleUploadSuccess}
resetData={resetData}
setAllConversations={setAllConversations}
/>
{data && <AnalyticsCharts data={data} />}
</div>
</div>

{data && <ClusterVisualisation clusters={data.clusters} />}
{data && (
<ClusterVisualisation
conversations={allConversations}
clusters={data.clusters}
/>
)}
</>
);
}
Expand Down
59 changes: 59 additions & 0 deletions ui/src/components/ChatDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Conversation } from "@/types/conversation";

import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { Markdown } from "./markdown";

type Props = {
chatId: string;
conversation: Conversation;
};

const ChatDialog = ({ chatId, conversation }: Props) => {
console.log(conversation);
return (
<Dialog>
<DialogTrigger>
<span className="text-sm text-muted-foreground hover:text-foreground p-2 font-mono border border-2">
{chatId}
</span>
</DialogTrigger>
<DialogContent className="bg-white max-w-4xl">
<DialogHeader>
<DialogTitle>Chat ID: {chatId}</DialogTitle>
</DialogHeader>
<DialogDescription className="space-y-4 max-h-[600px] overflow-y-auto px-2 py-4">
{conversation.messages.map((m, index) => (
<div
key={index}
className={`flex flex-col my-2 ${
m.role === "user" ? "items-end" : "items-start"
}`}
>
<div
className={`max-w-[80%] rounded-xl px-4 py-2 ${
m.role === "user"
? "bg-blue-500 text-white rounded-br-none"
: "bg-gray-200 text-gray-800 rounded-bl-none"
}`}
>
<Markdown>{m.content}</Markdown>
</div>
<span className="text-xs text-gray-500 mt-1">
{m.role === "user" ? "You" : "Assistant"}
</span>
</div>
))}
</DialogDescription>
</DialogContent>
</Dialog>
);
};

export default ChatDialog;
24 changes: 20 additions & 4 deletions ui/src/components/cluster-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { Badge } from "@/components/ui/badge";
import { FolderOpen, MessageCircle } from "lucide-react";
import { Cluster } from "@/types/analytics";
import { Conversation } from "@/types/conversation";
import { Dialog } from "./ui/dialog";
import ChatDialog from "./ChatDialog";

interface ClusterDetailProps {
cluster: Cluster | null;
conversations: Conversation[];
}

export default function ClusterDetail({ cluster }: ClusterDetailProps) {
export default function ClusterDetail({
cluster,
conversations,
}: ClusterDetailProps) {
if (!cluster) {
return (
<div className="h-full flex items-center justify-center text-muted-foreground">
Expand Down Expand Up @@ -61,9 +68,18 @@ export default function ClusterDetail({ cluster }: ClusterDetailProps) {

<div className="space-y-3">
<h3 className="text-base font-semibold">Chat IDs</h3>
<div className="text-sm text-muted-foreground bg-muted/40 rounded-lg p-4 font-mono break-all">
{cluster.chat_ids.slice(0, 5).join(",\n")}
{cluster.chat_ids.length > 5 && "..."}
<div className="space-y-4 max-h-[200px] overflow-y-auto p-2">
{cluster.chat_ids.map((chatId) => (
<ChatDialog
key={chatId}
chatId={chatId}
conversation={
conversations.find(
(c) => c.chat_id === chatId
) as Conversation
}
/>
))}
</div>
</div>

Expand Down
31 changes: 16 additions & 15 deletions ui/src/components/cluster-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChevronRight, ChevronDown, FolderTree } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Cluster } from "@/types/analytics";
import { cn } from "@/lib/utils";

interface ClusterTreeProps {
clusters: Cluster[];
Expand All @@ -16,6 +17,7 @@ interface ClusterTreeProps {
interface ClusterTreeItemProps {
cluster: Cluster;
allClusters: Cluster[];

level: number;
selectedClusterId: string | null;
onSelectCluster: (cluster: Cluster) => void;
Expand All @@ -40,16 +42,13 @@ function ClusterTreeItem({
return (
<div className="text-md text-left">
<div
className={`
flex items-center gap-2 py-2 px-3 hover:bg-accent/50 rounded-md cursor-pointer
transition-colors duration-200
${
selectedClusterId === cluster.id
? "bg-accent text-accent-foreground"
: ""
}
`}
style={{ paddingLeft: `${level * 16 + 12}px` }}
className={cn(
"flex items-center gap-2 py-2 px-3 hover:bg-accent/50 rounded-md cursor-pointer transition-colors duration-200 ",
selectedClusterId === cluster.id
? "bg-accent text-accent-foreground"
: ""
)}
style={{ paddingLeft: `${level * 30 + 12}px` }}
onClick={() => onSelectCluster(cluster)}
>
{childClusters.length > 0 ? (
Expand All @@ -71,15 +70,17 @@ function ClusterTreeItem({
) : (
<div className="w-4" />
)}
<span className="flex-1 font-medium truncate">{cluster.name}</span>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{percentage.toFixed(2)}% • {cluster.count.toLocaleString()}
{childClusters.length > 0 && ` • ${childClusters.length}`}
</span>
<div className="flex justify-between w-full">
<span className="flex-1 font-medium max-w-lg">{cluster.name}</span>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{cluster.count.toLocaleString()} items | {percentage.toFixed(2)}%
</span>
</div>
</div>
{isExpanded &&
childClusters.map((child) => (
<ClusterTreeItem
conversations={conversations}
key={child.id}
cluster={child}
allClusters={allClusters}
Expand Down
Loading

0 comments on commit 6caaf7c

Please sign in to comment.