mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-24 16:09:19 +02:00
Compare commits
1 Commits
agent/lamb
...
fix/commen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
238cec514d |
@@ -3,6 +3,7 @@
|
||||
import { useRef } from "react";
|
||||
import { Paperclip } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { cn } from "@multica/ui/lib/utils";
|
||||
|
||||
interface FileUploadButtonProps {
|
||||
@@ -33,24 +34,22 @@ function FileUploadButton({
|
||||
};
|
||||
|
||||
const iconSize = size === "sm" ? "h-3.5 w-3.5" : "h-4 w-4";
|
||||
const btnSize = size === "sm" ? "h-6 w-6" : "h-7 w-7";
|
||||
const buttonSize = size === "sm" ? "icon-xs" : "icon-sm";
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size={buttonSize}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
disabled={disabled}
|
||||
aria-label={attachLabel}
|
||||
title={attachLabel}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:opacity-50 disabled:pointer-events-none",
|
||||
btnSize,
|
||||
className,
|
||||
)}
|
||||
className={cn("text-muted-foreground", className)}
|
||||
>
|
||||
<Paperclip className={iconSize} />
|
||||
</button>
|
||||
</Button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
|
||||
@@ -564,34 +564,33 @@ function CommentRow({
|
||||
attachments={edit.editorAttachments}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
{edit.standaloneEditAttachments.length > 0 && (
|
||||
<AttachmentList
|
||||
attachments={edit.standaloneEditAttachments}
|
||||
className="max-w-full"
|
||||
onRemove={(attachmentId) =>
|
||||
edit.setRetainedStandaloneIds((ids) => {
|
||||
const next = new Set(ids ?? []);
|
||||
next.delete(attachmentId);
|
||||
return next;
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{edit.standaloneEditAttachments.length > 0 && (
|
||||
<AttachmentList
|
||||
attachments={edit.standaloneEditAttachments}
|
||||
className="mt-2 max-w-full"
|
||||
onRemove={(attachmentId) =>
|
||||
edit.setRetainedStandaloneIds((ids) => {
|
||||
const next = new Set(ids ?? []);
|
||||
next.delete(attachmentId);
|
||||
return next;
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between gap-2 mt-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<CommentTriggerChips
|
||||
agents={edit.triggerPreview.agents}
|
||||
suppressedAgentIds={edit.suppressedAgentIds}
|
||||
onToggle={edit.toggleSuppressedAgent}
|
||||
context="edit"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<FileUploadButton
|
||||
size="sm"
|
||||
multiple
|
||||
onSelect={(file) => edit.editorRef.current?.uploadFile(file)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="ghost" onClick={edit.cancelEdit}>{t(($) => $.comment.cancel_edit)}</Button>
|
||||
<Button size="sm" variant="outline" onClick={edit.saveEdit}>{t(($) => $.comment.save_action)}</Button>
|
||||
</div>
|
||||
@@ -864,7 +863,6 @@ function CommentCardImpl({
|
||||
agents={edit.triggerPreview.agents}
|
||||
suppressedAgentIds={edit.suppressedAgentIds}
|
||||
onToggle={edit.toggleSuppressedAgent}
|
||||
context="edit"
|
||||
/>
|
||||
<FileUploadButton
|
||||
size="sm"
|
||||
|
||||
@@ -155,7 +155,6 @@ function CommentInput({ issueId, onSubmit }: CommentInputProps) {
|
||||
agents={triggerPreview.agents}
|
||||
suppressedAgentIds={suppressedAgentIds}
|
||||
onToggle={toggleSuppressedAgent}
|
||||
context="comment"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-1 right-1.5 flex items-center gap-1">
|
||||
|
||||
@@ -45,7 +45,6 @@ describe("CommentTriggerChips", () => {
|
||||
);
|
||||
|
||||
const chip = screen.getByRole("button");
|
||||
expect(chip).toHaveTextContent("Comment");
|
||||
expect(chip).toHaveTextContent("Starts working when sent");
|
||||
expect(chip).toHaveAttribute("aria-pressed", "false");
|
||||
|
||||
@@ -53,19 +52,6 @@ describe("CommentTriggerChips", () => {
|
||||
expect(onToggle).toHaveBeenCalledWith("agent-1");
|
||||
});
|
||||
|
||||
it("labels reply trigger chips by composer context", () => {
|
||||
renderWithI18n(
|
||||
<CommentTriggerChips
|
||||
agents={[walt]}
|
||||
suppressedAgentIds={new Set()}
|
||||
onToggle={vi.fn()}
|
||||
context="reply"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button")).toHaveTextContent("Reply");
|
||||
});
|
||||
|
||||
it("dims a suppressed single agent into the skip state", () => {
|
||||
renderWithI18n(
|
||||
<CommentTriggerChips
|
||||
@@ -123,13 +109,12 @@ describe("CommentTriggerChips", () => {
|
||||
agents={[walt, bob]}
|
||||
suppressedAgentIds={new Set()}
|
||||
onToggle={onToggle}
|
||||
context="reply"
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
|
||||
expect(screen.getByText("This reply will trigger")).toBeInTheDocument();
|
||||
expect(screen.getByText("This comment will trigger")).toBeInTheDocument();
|
||||
const row = screen.getByRole("button", { name: /Bob/ });
|
||||
expect(row).toHaveTextContent("Bob");
|
||||
fireEvent.click(row);
|
||||
|
||||
@@ -29,11 +29,9 @@ interface CommentTriggerChipsProps {
|
||||
agents: CommentTriggerPreviewAgent[];
|
||||
suppressedAgentIds: Set<string>;
|
||||
onToggle: (agentId: string) => void;
|
||||
context?: CommentTriggerContext;
|
||||
}
|
||||
|
||||
type IssuesT = ReturnType<typeof useT<"issues">>["t"];
|
||||
type CommentTriggerContext = "comment" | "reply" | "edit";
|
||||
|
||||
function sourceLabel(source: string, t: IssuesT): string {
|
||||
switch (source) {
|
||||
@@ -61,28 +59,6 @@ function sourceReason(agent: CommentTriggerPreviewAgent, t: IssuesT): string {
|
||||
}
|
||||
}
|
||||
|
||||
function triggerContextLabel(context: CommentTriggerContext, t: IssuesT): string {
|
||||
switch (context) {
|
||||
case "reply":
|
||||
return t(($) => $.comment.trigger_context_reply);
|
||||
case "edit":
|
||||
return t(($) => $.comment.trigger_context_edit);
|
||||
default:
|
||||
return t(($) => $.comment.trigger_context_comment);
|
||||
}
|
||||
}
|
||||
|
||||
function triggerPreviewTitle(context: CommentTriggerContext, t: IssuesT): string {
|
||||
switch (context) {
|
||||
case "reply":
|
||||
return t(($) => $.comment.trigger_preview_title_reply);
|
||||
case "edit":
|
||||
return t(($) => $.comment.trigger_preview_title_edit);
|
||||
default:
|
||||
return t(($) => $.comment.trigger_preview_title_comment);
|
||||
}
|
||||
}
|
||||
|
||||
// Presence is display metadata only — the trigger list itself is always the
|
||||
// backend preview. Online-ish agents start right away; offline ones queue.
|
||||
function useTriggerPresenceLine(agentId: string, t: IssuesT): string | null {
|
||||
@@ -131,7 +107,6 @@ export function CommentTriggerChips({
|
||||
agents,
|
||||
suppressedAgentIds,
|
||||
onToggle,
|
||||
context = "comment",
|
||||
}: CommentTriggerChipsProps) {
|
||||
const { t } = useT("issues");
|
||||
|
||||
@@ -146,7 +121,6 @@ export function CommentTriggerChips({
|
||||
agent={agent}
|
||||
suppressed={suppressedAgentIds.has(agent.id)}
|
||||
onToggle={onToggle}
|
||||
context={context}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
@@ -157,42 +131,20 @@ export function CommentTriggerChips({
|
||||
agents={agents}
|
||||
suppressedAgentIds={suppressedAgentIds}
|
||||
onToggle={onToggle}
|
||||
context={context}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TriggerContextPrefix({
|
||||
context,
|
||||
t,
|
||||
}: {
|
||||
context: CommentTriggerContext;
|
||||
t: IssuesT;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<span className="shrink-0 text-[10px] font-semibold tracking-normal text-muted-foreground/80">
|
||||
{triggerContextLabel(context, t)}
|
||||
</span>
|
||||
<span aria-hidden="true" className="shrink-0 text-muted-foreground/50">
|
||||
·
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SingleTriggerChip({
|
||||
agent,
|
||||
suppressed,
|
||||
onToggle,
|
||||
context,
|
||||
t,
|
||||
}: {
|
||||
agent: CommentTriggerPreviewAgent;
|
||||
suppressed: boolean;
|
||||
onToggle: (agentId: string) => void;
|
||||
context: CommentTriggerContext;
|
||||
t: IssuesT;
|
||||
}) {
|
||||
const state = suppressed
|
||||
@@ -220,7 +172,6 @@ function SingleTriggerChip({
|
||||
suppressed && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<TriggerContextPrefix context={context} t={t} />
|
||||
<TriggerAgentAvatar agent={agent} suppressed={suppressed} />
|
||||
<span className="truncate">{sentence}</span>
|
||||
</button>
|
||||
@@ -237,13 +188,11 @@ function MultiTriggerChip({
|
||||
agents,
|
||||
suppressedAgentIds,
|
||||
onToggle,
|
||||
context,
|
||||
t,
|
||||
}: {
|
||||
agents: CommentTriggerPreviewAgent[];
|
||||
suppressedAgentIds: Set<string>;
|
||||
onToggle: (agentId: string) => void;
|
||||
context: CommentTriggerContext;
|
||||
t: IssuesT;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -276,7 +225,6 @@ function MultiTriggerChip({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TriggerContextPrefix context={context} t={t} />
|
||||
<span className="inline-flex items-center">
|
||||
{heads.map((agent, i) => (
|
||||
<span
|
||||
@@ -319,7 +267,7 @@ function MultiTriggerChip({
|
||||
</Tooltip>
|
||||
<PopoverContent align="start" className="w-64 p-2">
|
||||
<div className="px-1.5 pb-1 text-xs font-medium text-muted-foreground">
|
||||
{triggerPreviewTitle(context, t)}
|
||||
{t(($) => $.comment.trigger_preview_title)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{agents.map((agent) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRef, useState, useCallback, useEffect } from "react";
|
||||
import { ArrowUp, Loader2 } from "lucide-react";
|
||||
import { ContentEditor, type ContentEditorRef, useFileDropZone, FileDropOverlay } from "../../editor";
|
||||
import { FileUploadButton } from "@multica/ui/components/common/file-upload-button";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { ActorAvatar } from "../../common/actor-avatar";
|
||||
import { useFileUpload } from "@multica/core/hooks/use-file-upload";
|
||||
import { api } from "@multica/core/api";
|
||||
@@ -189,7 +190,6 @@ function ReplyInput({
|
||||
agents={triggerPreview.agents}
|
||||
suppressedAgentIds={suppressedAgentIds}
|
||||
onToggle={toggleSuppressedAgent}
|
||||
context="reply"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-0 right-0 flex items-center gap-1">
|
||||
@@ -198,23 +198,19 @@ function ReplyInput({
|
||||
multiple
|
||||
onSelect={(file) => editorRef.current?.uploadFile(file)}
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant={isEmpty ? "ghost" : "default"}
|
||||
size="icon-xs"
|
||||
disabled={isEmpty || submitting}
|
||||
onClick={handleSubmit}
|
||||
className={cn(
|
||||
"inline-flex h-6 w-6 items-center justify-center rounded-full transition-colors disabled:pointer-events-none disabled:opacity-50",
|
||||
isEmpty
|
||||
? "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
)}
|
||||
>
|
||||
{submitting ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<ArrowUp className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
{isDragOver && <FileDropOverlay />}
|
||||
</div>
|
||||
|
||||
@@ -279,9 +279,6 @@
|
||||
"trigger_source_mention_agent": "@mention",
|
||||
"trigger_source_mention_squad_leader": "squad",
|
||||
"trigger_source_unknown": "trigger",
|
||||
"trigger_context_comment": "Comment",
|
||||
"trigger_context_reply": "Reply",
|
||||
"trigger_context_edit": "Edit",
|
||||
"trigger_skipped_label": "Skipped",
|
||||
"trigger_wont_trigger": "Won't be triggered",
|
||||
"trigger_none_will_trigger": "No agents will be triggered",
|
||||
@@ -298,9 +295,6 @@
|
||||
"trigger_click_to_manage": "Click to manage who gets triggered this time.",
|
||||
"trigger_click_to_restore": "Won't be triggered this time. Click to restore.",
|
||||
"trigger_preview_title": "This comment will trigger",
|
||||
"trigger_preview_title_comment": "This comment will trigger",
|
||||
"trigger_preview_title_reply": "This reply will trigger",
|
||||
"trigger_preview_title_edit": "This edit will trigger",
|
||||
"trigger_chip_aria": "{{name}} trigger: {{state}}",
|
||||
"resolve": {
|
||||
"resolve_thread_action": "Resolve thread",
|
||||
|
||||
@@ -271,9 +271,6 @@
|
||||
"trigger_source_mention_agent": "@メンション",
|
||||
"trigger_source_mention_squad_leader": "Squad",
|
||||
"trigger_source_unknown": "トリガー",
|
||||
"trigger_context_comment": "コメント",
|
||||
"trigger_context_reply": "返信",
|
||||
"trigger_context_edit": "編集",
|
||||
"trigger_skipped_label": "スキップ",
|
||||
"trigger_wont_trigger": "トリガーされません",
|
||||
"trigger_none_will_trigger": "いずれもトリガーされません",
|
||||
@@ -289,9 +286,6 @@
|
||||
"trigger_click_to_manage": "クリックして今回のトリガー対象を管理します。",
|
||||
"trigger_click_to_restore": "今回はトリガーされません。クリックで元に戻せます。",
|
||||
"trigger_preview_title": "このコメントでトリガーされる対象",
|
||||
"trigger_preview_title_comment": "このコメントでトリガーされる対象",
|
||||
"trigger_preview_title_reply": "この返信でトリガーされる対象",
|
||||
"trigger_preview_title_edit": "この編集でトリガーされる対象",
|
||||
"trigger_chip_aria": "{{name}} のトリガー: {{state}}",
|
||||
"resolve": {
|
||||
"resolve_thread_action": "スレッドを解決",
|
||||
|
||||
@@ -279,9 +279,6 @@
|
||||
"trigger_source_mention_agent": "@멘션",
|
||||
"trigger_source_mention_squad_leader": "Squad",
|
||||
"trigger_source_unknown": "트리거",
|
||||
"trigger_context_comment": "댓글",
|
||||
"trigger_context_reply": "답글",
|
||||
"trigger_context_edit": "편집",
|
||||
"trigger_skipped_label": "건너뜀",
|
||||
"trigger_wont_trigger": "트리거되지 않습니다",
|
||||
"trigger_none_will_trigger": "모두 트리거되지 않습니다",
|
||||
@@ -297,9 +294,6 @@
|
||||
"trigger_click_to_manage": "클릭하여 이번 트리거 대상을 관리합니다.",
|
||||
"trigger_click_to_restore": "이번에는 트리거되지 않습니다. 클릭하면 복원됩니다.",
|
||||
"trigger_preview_title": "이 댓글로 트리거되는 대상",
|
||||
"trigger_preview_title_comment": "이 댓글로 트리거되는 대상",
|
||||
"trigger_preview_title_reply": "이 답글로 트리거되는 대상",
|
||||
"trigger_preview_title_edit": "이 편집으로 트리거되는 대상",
|
||||
"trigger_chip_aria": "{{name}} 트리거: {{state}}",
|
||||
"resolve": {
|
||||
"resolve_thread_action": "스레드 해결",
|
||||
|
||||
@@ -276,9 +276,6 @@
|
||||
"trigger_source_mention_agent": "@mention",
|
||||
"trigger_source_mention_squad_leader": "squad",
|
||||
"trigger_source_unknown": "将触发",
|
||||
"trigger_context_comment": "评论",
|
||||
"trigger_context_reply": "回复",
|
||||
"trigger_context_edit": "编辑",
|
||||
"trigger_skipped_label": "已跳过",
|
||||
"trigger_wont_trigger": "不会触发",
|
||||
"trigger_none_will_trigger": "全部不会触发",
|
||||
@@ -294,9 +291,6 @@
|
||||
"trigger_click_to_manage": "点击管理本次触发对象。",
|
||||
"trigger_click_to_restore": "本次不触发,点击恢复。",
|
||||
"trigger_preview_title": "这条评论将触发",
|
||||
"trigger_preview_title_comment": "这条评论将触发",
|
||||
"trigger_preview_title_reply": "这条回复将触发",
|
||||
"trigger_preview_title_edit": "本次编辑将触发",
|
||||
"trigger_chip_aria": "{{name}} 触发状态:{{state}}",
|
||||
"resolve": {
|
||||
"resolve_thread_action": "解决该讨论",
|
||||
|
||||
Reference in New Issue
Block a user