Skip to content

Commit 1ddfcd5

Browse files
feat: Implement CanvasCard component and integrate into GalleryPage and GithubGalleryPage (#14)
fix: Update release notes handling in GitHub Actions workflow chore: Bump version to 0.1.5 and remove deprecated logging in server Co-authored-by: BigMiao <g2260578356@gmail.com>
1 parent 9f7082c commit 1ddfcd5

File tree

8 files changed

+481
-199
lines changed

8 files changed

+481
-199
lines changed

.github/workflows/release.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,11 @@ jobs:
140140
echo "First release of llm-canvas" >> release_notes.md
141141
fi
142142
143-
echo "release_notes<<EOF" >> $GITHUB_OUTPUT
144-
cat release_notes.md >> $GITHUB_OUTPUT
145-
echo "EOF" >> $GITHUB_OUTPUT
143+
{
144+
echo "release_notes<<EOF"
145+
cat release_notes.md
146+
echo "EOF"
147+
} >> $GITHUB_OUTPUT
146148
147149
- name: Create GitHub Release
148150
uses: softprops/action-gh-release@v2

llm_canvas/_server/_server.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ def start_local_server(host: str = "127.0.0.1", port: int = 8000, log_level: str
9999
logger.warning(" • Data is lost when server restarts")
100100
logger.warning(" • No backup or recovery mechanisms")
101101
logger.warning(" • Session-based storage only")
102-
logger.info(" For permanent storage, consider cloud plans")
103102

104103
app = create_local_server()
105104

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "llm-canvas"
3-
version = "0.1.4"
3+
version = "0.1.5"
44
description = "Visualize complex LLM conversation flows in infinite canvases"
55
authors = [ { name = "LittleLittleCloud" } ]
66
readme = "README.md"

web_ui/src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import config from "./config";
66
import "./index.css";
77
import { CanvasPage } from "./pages/CanvasPage";
88
import { GalleryPage } from "./pages/GalleryPage";
9+
import { GithubGalleryPage } from "./pages/GithubGalleryPage";
910
import { GithubLandingPage } from "./pages/GithubLandingPage";
1011

1112
export const App: React.FC = () => {
@@ -24,7 +25,7 @@ export const App: React.FC = () => {
2425
element={isGithubMode ? <GithubLandingPage /> : <GalleryPage />}
2526
/>
2627
{isGithubMode && (
27-
<Route path="/gallery" element={<GalleryPage />} />
28+
<Route path="/gallery" element={<GithubGalleryPage />} />
2829
)}
2930
<Route path="/canvas/:id" element={<CanvasPage />} />
3031
<Route

web_ui/src/components/CanvasCard.tsx

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import React from "react";
2+
import { CanvasSummary } from "../types";
3+
4+
interface CanvasCardProps {
5+
canvas: CanvasSummary;
6+
onOpen: (id: string) => void;
7+
onDelete?: (id: string, e: React.MouseEvent) => void;
8+
showDeleteButton?: boolean;
9+
isDeleting?: boolean;
10+
}
11+
12+
const formatTs = (ts: number) => {
13+
try {
14+
const d = new Date(ts * 1000);
15+
return d.toLocaleDateString("en-US", {
16+
month: "short",
17+
day: "numeric",
18+
year: "numeric",
19+
});
20+
} catch {
21+
return "";
22+
}
23+
};
24+
25+
const formatTimeAgo = (ts: number) => {
26+
try {
27+
const now = Date.now();
28+
const diff = now - ts * 1000;
29+
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
30+
const hours = Math.floor(diff / (1000 * 60 * 60));
31+
const minutes = Math.floor(diff / (1000 * 60));
32+
33+
if (days > 0) return `${days}d ago`;
34+
if (hours > 0) return `${hours}h ago`;
35+
if (minutes > 0) return `${minutes}m ago`;
36+
return "Just now";
37+
} catch {
38+
return "";
39+
}
40+
};
41+
42+
export const CanvasCard: React.FC<CanvasCardProps> = ({
43+
canvas,
44+
onOpen,
45+
onDelete,
46+
showDeleteButton = true,
47+
isDeleting = false,
48+
}) => {
49+
return (
50+
<div
51+
key={canvas.canvas_id}
52+
className="group relative bg-white dark:bg-gray-800 rounded-2xl shadow-sm hover:shadow-xl border border-gray-100 dark:border-gray-700 overflow-hidden transition-all duration-300 hover:scale-[1.02] hover:-translate-y-1"
53+
>
54+
{/* Canvas Header with Gradient */}
55+
<div className="h-2 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500" />
56+
57+
{/* Canvas Content */}
58+
<div className="relative">
59+
{/* Main clickable area */}
60+
<button
61+
onClick={() => onOpen(canvas.canvas_id)}
62+
className="w-full p-6 text-left focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-inset"
63+
>
64+
{/* Title */}
65+
<div className="mb-2">
66+
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 truncate group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors">
67+
{canvas.title || "Untitled Canvas"}
68+
</h3>
69+
</div>
70+
71+
{/* Stats - right below title */}
72+
<div className="flex items-center gap-4 mb-4">
73+
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400">
74+
<svg
75+
className="w-4 h-4 mr-1"
76+
fill="none"
77+
stroke="currentColor"
78+
viewBox="0 0 24 24"
79+
>
80+
<path
81+
strokeLinecap="round"
82+
strokeLinejoin="round"
83+
strokeWidth={2}
84+
d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"
85+
/>
86+
</svg>
87+
{canvas.node_count} nodes
88+
</div>
89+
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400">
90+
<svg
91+
className="w-4 h-4 mr-1"
92+
fill="none"
93+
stroke="currentColor"
94+
viewBox="0 0 24 24"
95+
>
96+
<path
97+
strokeLinecap="round"
98+
strokeLinejoin="round"
99+
strokeWidth={2}
100+
d="M13 10V3L4 14h7v7l9-11h-7z"
101+
/>
102+
</svg>
103+
{canvas.root_ids.length} roots
104+
</div>
105+
</div>
106+
107+
{/* Description - fixed height */}
108+
<div className="mb-4 h-16">
109+
{canvas.description ? (
110+
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-3 leading-relaxed">
111+
{canvas.description}
112+
</p>
113+
) : (
114+
<p className="text-sm text-gray-400 dark:text-gray-500 italic">
115+
No description provided
116+
</p>
117+
)}
118+
</div>
119+
120+
{/* Footer */}
121+
<div className="flex items-center justify-between pt-4 border-t border-gray-100 dark:border-gray-700">
122+
<div className="text-xs text-gray-500 dark:text-gray-400">
123+
Created {formatTs(canvas.created_at)}
124+
</div>
125+
<div className="text-xs text-gray-400 dark:text-gray-500">
126+
{formatTimeAgo(canvas.created_at)}
127+
</div>
128+
</div>
129+
130+
{/* Hover indicator */}
131+
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300" />
132+
</button>
133+
134+
{/* Delete button positioned absolutely */}
135+
{showDeleteButton && onDelete && (
136+
<button
137+
onClick={e => onDelete(canvas.canvas_id, e)}
138+
disabled={isDeleting}
139+
className="absolute top-4 right-4 p-2 text-gray-400 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-400 focus:outline-none focus:text-red-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed bg-white dark:bg-gray-800 rounded-lg shadow-sm opacity-0 group-hover:opacity-100 hover:shadow-md"
140+
title="Delete canvas"
141+
>
142+
{isDeleting ? (
143+
<svg
144+
className="w-4 h-4 animate-spin"
145+
fill="none"
146+
viewBox="0 0 24 24"
147+
>
148+
<circle
149+
className="opacity-25"
150+
cx="12"
151+
cy="12"
152+
r="10"
153+
stroke="currentColor"
154+
strokeWidth="4"
155+
/>
156+
<path
157+
className="opacity-75"
158+
fill="currentColor"
159+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
160+
/>
161+
</svg>
162+
) : (
163+
<svg
164+
className="w-4 h-4"
165+
fill="none"
166+
stroke="currentColor"
167+
viewBox="0 0 24 24"
168+
>
169+
<path
170+
strokeLinecap="round"
171+
strokeLinejoin="round"
172+
strokeWidth={2}
173+
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
174+
/>
175+
</svg>
176+
)}
177+
</button>
178+
)}
179+
</div>
180+
</div>
181+
);
182+
};

0 commit comments

Comments
 (0)