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
41 changes: 41 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"permissions": {
"allow": [
"Bash(pnpm --version:*)",
"Bash(docker:*)",
"Bash(node:*)",
"Bash(echo:*)",
"Bash(pnpm install:*)",
"Bash(curl:*)",
"Bash(sudo sh:*)",
"Bash(sudo usermod:*)",
"Bash(npx convex deploy)",
"Bash(pnpm dev)",
"Bash(sudo docker compose ps:*)",
"Bash(python3:*)",
"Bash(git commit:*)",
"Bash(openclaw:*)",
"Bash(dpkg:*)",
"Bash(snap list:*)",
"Bash(apt list:*)",
"Bash(pnpm --filter @clawe/shared test:*)",
"Bash(pnpm check:*)",
"Bash(pnpm prettier:*)",
"Bash(pnpm build:*)",
"Bash(pnpm test:*)",
"Bash(npx convex run:*)",
"Bash(npx convex dev:*)",
"Bash(pnpm add:*)",
"Bash(pnpm run format:check:*)",
"Bash(git -C /home/ubuntu/clawe config user.name)",
"Bash(git -C /home/ubuntu/clawe config user.email)",
"Bash(git -C /home/ubuntu/clawe config:*)",
"Bash(git -C /home/ubuntu/clawe log:*)",
"Bash(pnpm --filter web test:*)",
"Bash(npx prettier:*)",
"Bash(pnpm --filter @clawe/cli test:*)",
"Bash(pnpm ls:*)",
"Bash(pnpm fix:*)"
]
}
}
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,7 @@ docker/openclaw/data/
.data/

# OpenClaw state directory (shared with Docker in dev)
.openclaw/
.openclaw/*
!.openclaw/.gitkeep
.openclaw/logs/*
!.openclaw/logs/.gitkeep
Empty file added .openclaw/.gitkeep
Empty file.
26 changes: 16 additions & 10 deletions apps/web/src/app/(dashboard)/board/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import {
KanbanBoard,
type KanbanTask,
type KanbanSubtask,
type KanbanColumnDef,
} from "@/components/kanban";
import { LiveFeed, LiveFeedTitle } from "@/components/live-feed";
Expand All @@ -42,16 +43,21 @@ function mapPriority(priority?: string): "low" | "medium" | "high" {

// Map Convex task to Kanban task format
function mapTask(task: TaskWithAssignees): KanbanTask {
const subtasks: KanbanTask[] =
task.subtasks
?.filter((st) => !st.done)
.map((st, i) => ({
id: `${task._id}-${i}`,
title: st.title,
description: st.description,
priority: "medium",
subtasks: [],
})) || [];
const subtasks: KanbanSubtask[] =
task.subtasks?.map((st, i) => ({
id: `${task._id}-${i}`,
title: st.title,
description: st.description,
done: st.done || false,
status: st.status ?? (st.done ? "done" : "pending"),
blockedReason: st.blockedReason,
assignee: (() => {
if (!st.assigneeId) return undefined;
const agent = task.assignees?.find((a) => a._id === st.assigneeId);
return agent ? `${agent.emoji || ""} ${agent.name}`.trim() : undefined;
})(),
doneAt: st.doneAt,
})) || [];

return {
id: task._id,
Expand Down
109 changes: 97 additions & 12 deletions apps/web/src/components/kanban/_components/document-viewer-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useEffect, useState, useMemo } from "react";
import axios from "axios";
import {
Dialog,
Expand All @@ -16,6 +16,51 @@ import type { DocumentWithCreator } from "@clawe/backend/types";

const VIEWER_HEIGHT = "h-[500px]";

/** Very simple markdown → HTML renderer for preview mode */
function renderMarkdown(md: string): string {
const html = md
// Escape HTML
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
// Headers
.replace(
/^#### (.+)$/gm,
'<h4 class="text-sm font-semibold mt-4 mb-1">$1</h4>',
)
.replace(
/^### (.+)$/gm,
'<h3 class="text-base font-semibold mt-5 mb-1.5">$1</h3>',
)
.replace(/^## (.+)$/gm, '<h2 class="text-lg font-bold mt-6 mb-2">$1</h2>')
.replace(/^# (.+)$/gm, '<h1 class="text-xl font-bold mt-6 mb-2">$1</h1>')
// Bold & italic
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.+?)\*/g, "<em>$1</em>")
// Inline code
.replace(
/`([^`]+)`/g,
'<code class="rounded bg-gray-100 px-1 py-0.5 text-xs dark:bg-zinc-800">$1</code>',
)
// Unordered lists
.replace(/^- (.+)$/gm, '<li class="ml-4 list-disc">$1</li>')
// Ordered lists
.replace(/^\d+\. (.+)$/gm, '<li class="ml-4 list-decimal">$1</li>')
// Horizontal rules
.replace(
/^---$/gm,
'<hr class="my-4 border-gray-200 dark:border-zinc-700" />',
)
// Paragraphs (double newlines)
.replace(/\n\n/g, '</p><p class="mb-2">')
// Single newlines within paragraphs
.replace(/\n/g, "<br />");

return `<p class="mb-2">${html}</p>`;
}

type ViewMode = "preview" | "raw";

export type DocumentViewerModalProps = {
document: DocumentWithCreator | null;
open: boolean;
Expand All @@ -29,11 +74,13 @@ export const DocumentViewerModal = ({
}: DocumentViewerModalProps) => {
const [fileContent, setFileContent] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [viewMode, setViewMode] = useState<ViewMode>("preview");

useEffect(() => {
const fileUrl = document?.fileUrl;
if (!fileUrl || !open) {
setFileContent(null);
setViewMode("preview");
return;
}

Expand Down Expand Up @@ -63,24 +110,55 @@ export const DocumentViewerModal = ({
};
}, [document?.fileUrl, open]);

if (!document) return null;
const content = fileContent ?? document?.content;

const content = fileContent ?? document.content;
const previewHtml = useMemo(() => {
if (!content) return "";
return renderMarkdown(content);
}, [content]);

if (!document) return null;

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[80vh] w-full flex-col overflow-hidden sm:w-[95vw] sm:max-w-6xl">
<DialogHeader className="shrink-0">
<DialogTitle className="flex items-center justify-between gap-4 pr-8">
<span className="truncate">{document.title}</span>
{document.fileUrl && (
<Button variant="outline" size="sm" asChild className="shrink-0">
<a href={document.fileUrl} download={document.title}>
<Download className="mr-2 h-4 w-4" />
Download
</a>
</Button>
)}
<div className="flex shrink-0 items-center gap-2">
<div className="inline-flex items-center rounded-md border border-gray-200 dark:border-zinc-700">
<Button
variant="ghost"
size="sm"
className={cn(
"h-8 rounded-r-none border-r border-gray-200 px-3 text-xs dark:border-zinc-700",
viewMode === "preview" && "bg-gray-100 dark:bg-zinc-800",
)}
onClick={() => setViewMode("preview")}
>
Preview
</Button>
<Button
variant="ghost"
size="sm"
className={cn(
"h-8 rounded-l-none px-3 text-xs",
viewMode === "raw" && "bg-gray-100 dark:bg-zinc-800",
)}
onClick={() => setViewMode("raw")}
>
Raw
</Button>
</div>
{document.fileUrl && (
<Button variant="outline" size="sm" asChild>
<a href={document.fileUrl} download={document.title}>
<Download className="mr-2 h-4 w-4" />
Download
</a>
</Button>
)}
</div>
</DialogTitle>
</DialogHeader>

Expand All @@ -95,7 +173,14 @@ export const DocumentViewerModal = ({
</div>
) : content ? (
<div className={cn("overflow-auto rounded border", VIEWER_HEIGHT)}>
<pre className="p-4 text-sm whitespace-pre-wrap">{content}</pre>
{viewMode === "raw" ? (
<pre className="p-4 text-sm whitespace-pre-wrap">{content}</pre>
) : (
<div
className="prose prose-sm dark:prose-invert max-w-none p-4"
dangerouslySetInnerHTML={{ __html: previewHtml }}
/>
)}
</div>
) : (
<div
Expand Down
Loading