Add TKMind platform extensions, H5/MindSpace stack, and deployment tooling.
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Fork goose with custom MCP widgets, platform extensions (aider, git, web, search), MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
type CommentInputProps = {
|
||||
placeholder?: string;
|
||||
submitLabel?: string;
|
||||
onSubmit: (content: string) => Promise<void>;
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
export function CommentInput({
|
||||
placeholder = '写下你的评论…',
|
||||
submitLabel = '发布',
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: CommentInputProps) {
|
||||
const [content, setContent] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed || busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await onSubmit(trimmed);
|
||||
setContent('');
|
||||
onCancel?.();
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(event) => setContent(event.target.value)}
|
||||
rows={3}
|
||||
placeholder={placeholder}
|
||||
className="w-full resize-none rounded-xl border border-[#d6d0c3] bg-white px-4 py-3 text-sm text-[#17221d] outline-none focus:border-[#2f6f57]"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
{onCancel ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="rounded-full border border-[#d6d0c3] px-4 py-2 text-sm text-[#68716c]"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy || !content.trim()}
|
||||
onClick={handleSubmit}
|
||||
className="rounded-full bg-[#2f6f57] px-5 py-2 text-sm font-semibold text-white disabled:opacity-50"
|
||||
>
|
||||
{busy ? '发送中…' : submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import type { PlazaComment } from '@/types/plaza';
|
||||
import { CommentThread } from '@/components/comment/CommentThread';
|
||||
|
||||
type CommentListProps = {
|
||||
postId: string;
|
||||
comments: PlazaComment[];
|
||||
onReply: (comment: PlazaComment) => void;
|
||||
onToggleLike: (comment: PlazaComment) => void;
|
||||
};
|
||||
|
||||
export function CommentList({ postId, comments, onReply, onToggleLike }: CommentListProps) {
|
||||
if (comments.length === 0) {
|
||||
return <p className="py-6 text-center text-sm text-[#8a928c]">暂无评论,来抢沙发吧</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="divide-y divide-[#ebe4d6]">
|
||||
{comments.map((comment) => (
|
||||
<li key={comment.id} className="py-4">
|
||||
<CommentThread
|
||||
postId={postId}
|
||||
comment={comment}
|
||||
onReply={onReply}
|
||||
onToggleLike={onToggleLike}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { PlazaComment } from '@/types/plaza';
|
||||
import { LoginPrompt } from '@/components/auth/LoginPrompt';
|
||||
import { CommentInput } from '@/components/comment/CommentInput';
|
||||
import { CommentList } from '@/components/comment/CommentList';
|
||||
import {
|
||||
PlazaApiError,
|
||||
createComment,
|
||||
fetchComments,
|
||||
toggleCommentLike,
|
||||
} from '@/lib/api';
|
||||
|
||||
type CommentSectionProps = {
|
||||
postId: string;
|
||||
allowComment?: boolean;
|
||||
};
|
||||
|
||||
export function CommentSection({ postId, allowComment = true }: CommentSectionProps) {
|
||||
const [comments, setComments] = useState<PlazaComment[]>([]);
|
||||
const [cursor, setCursor] = useState<string | null>(null);
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [replyTo, setReplyTo] = useState<PlazaComment | null>(null);
|
||||
const [loginOpen, setLoginOpen] = useState(false);
|
||||
|
||||
const loadComments = useCallback(
|
||||
async (nextCursor?: string | null) => {
|
||||
const data = await fetchComments(postId, { cursor: nextCursor ?? undefined });
|
||||
if (nextCursor) {
|
||||
setComments((current) => [...current, ...data.comments]);
|
||||
} else {
|
||||
setComments(data.comments);
|
||||
}
|
||||
setCursor(data.next_cursor);
|
||||
setHasMore(data.has_more);
|
||||
},
|
||||
[postId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
loadComments()
|
||||
.catch(() => {
|
||||
if (!cancelled) setComments([]);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [loadComments]);
|
||||
|
||||
const handleCreate = async (content: string) => {
|
||||
try {
|
||||
await createComment(postId, content, replyTo?.id ?? null);
|
||||
setReplyTo(null);
|
||||
await loadComments();
|
||||
} catch (error) {
|
||||
if (error instanceof PlazaApiError && error.code === 'unauthorized') {
|
||||
setLoginOpen(true);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleLike = async (comment: PlazaComment) => {
|
||||
try {
|
||||
const liked = !comment.viewer_liked;
|
||||
await toggleCommentLike(comment.id, liked);
|
||||
setComments((current) =>
|
||||
current.map((item) =>
|
||||
item.id === comment.id
|
||||
? {
|
||||
...item,
|
||||
viewer_liked: liked,
|
||||
like_count: Math.max(0, item.like_count + (liked ? 1 : -1)),
|
||||
}
|
||||
: item,
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof PlazaApiError && error.code === 'unauthorized') {
|
||||
setLoginOpen(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadMore = async () => {
|
||||
if (!cursor || loadingMore) return;
|
||||
setLoadingMore(true);
|
||||
try {
|
||||
await loadComments(cursor);
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!allowComment) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-[#d6d0c3] bg-[#fffdf7] px-5 py-4 text-sm text-[#68716c]">
|
||||
作者已关闭评论
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl border border-[#d6d0c3] bg-[#fffdf7] px-5 py-5">
|
||||
<h2 className="text-lg font-semibold text-[#17221d]">评论</h2>
|
||||
<div className="mt-4">
|
||||
{replyTo ? (
|
||||
<p className="mb-2 text-sm text-[#68716c]">
|
||||
回复 @{replyTo.author.display_name}
|
||||
</p>
|
||||
) : null}
|
||||
<CommentInput
|
||||
placeholder={replyTo ? `回复 @${replyTo.author.display_name}` : '写下你的评论…'}
|
||||
onSubmit={handleCreate}
|
||||
onCancel={replyTo ? () => setReplyTo(null) : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
{loading ? (
|
||||
<p className="py-6 text-center text-sm text-[#8a928c]">加载评论中…</p>
|
||||
) : (
|
||||
<CommentList
|
||||
postId={postId}
|
||||
comments={comments}
|
||||
onReply={setReplyTo}
|
||||
onToggleLike={handleToggleLike}
|
||||
/>
|
||||
)}
|
||||
{hasMore ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={loadingMore}
|
||||
onClick={handleLoadMore}
|
||||
className="mt-4 w-full rounded-full border border-[#d6d0c3] py-2.5 text-sm font-semibold text-[#2f6f57]"
|
||||
>
|
||||
{loadingMore ? '加载中…' : '加载更多评论'}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<LoginPrompt
|
||||
open={loginOpen}
|
||||
onClose={() => setLoginOpen(false)}
|
||||
message="登录后即可发表评论"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import type { PlazaComment } from '@/types/plaza';
|
||||
import { formatCount, formatRelativeTime } from '@/lib/format';
|
||||
import { fetchComments } from '@/lib/api';
|
||||
import { CommentReportButton } from '@/components/post/CommentReportButton';
|
||||
|
||||
type CommentThreadProps = {
|
||||
postId: string;
|
||||
comment: PlazaComment;
|
||||
depth?: number;
|
||||
onReply: (comment: PlazaComment) => void;
|
||||
onToggleLike: (comment: PlazaComment) => void;
|
||||
};
|
||||
|
||||
export function CommentThread({
|
||||
postId,
|
||||
comment,
|
||||
depth = 0,
|
||||
onReply,
|
||||
onToggleLike,
|
||||
}: CommentThreadProps) {
|
||||
const [replies, setReplies] = useState<PlazaComment[]>([]);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [loadingReplies, setLoadingReplies] = useState(false);
|
||||
|
||||
const loadReplies = async () => {
|
||||
if (loadingReplies || expanded) return;
|
||||
setLoadingReplies(true);
|
||||
try {
|
||||
const data = await fetchComments(postId, { parent_id: comment.id });
|
||||
setReplies(data.comments);
|
||||
setExpanded(true);
|
||||
} finally {
|
||||
setLoadingReplies(false);
|
||||
}
|
||||
};
|
||||
|
||||
const canReply = depth === 0;
|
||||
|
||||
return (
|
||||
<div className={depth > 0 ? 'border-l border-[#ebe4d6] pl-4' : undefined}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link
|
||||
href={`/u/${comment.author.slug}`}
|
||||
className="font-semibold text-[#17221d] hover:text-[#2f6f57]"
|
||||
>
|
||||
{comment.author.display_name}
|
||||
</Link>
|
||||
<p className="mt-1 text-sm leading-relaxed text-[#4a5751]">{comment.content}</p>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-3 text-xs text-[#8a928c]">
|
||||
<span>{formatRelativeTime(comment.created_at)}</span>
|
||||
{canReply ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onReply(comment)}
|
||||
className="font-semibold text-[#68716c] hover:text-[#2f6f57]"
|
||||
>
|
||||
回复
|
||||
</button>
|
||||
) : null}
|
||||
<CommentReportButton commentId={comment.id} />
|
||||
</div>
|
||||
{canReply && comment.reply_count > 0 && !expanded ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void loadReplies()}
|
||||
className="mt-2 text-xs font-semibold text-[#2f6f57]"
|
||||
>
|
||||
{loadingReplies
|
||||
? '加载回复中…'
|
||||
: `查看 ${formatCount(comment.reply_count)} 条回复`}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onToggleLike(comment)}
|
||||
className={`shrink-0 rounded-full px-3 py-1 text-xs font-semibold ${
|
||||
comment.viewer_liked
|
||||
? 'bg-[#2f6f57] text-white'
|
||||
: 'border border-[#d6d0c3] text-[#68716c]'
|
||||
}`}
|
||||
>
|
||||
👍 {formatCount(comment.like_count)}
|
||||
</button>
|
||||
</div>
|
||||
{expanded && replies.length > 0 ? (
|
||||
<ul className="mt-3 space-y-3">
|
||||
{replies.map((reply) => (
|
||||
<li key={reply.id}>
|
||||
<CommentThread
|
||||
postId={postId}
|
||||
comment={reply}
|
||||
depth={depth + 1}
|
||||
onReply={onReply}
|
||||
onToggleLike={onToggleLike}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user