@agent-native/core 0.80.2 → 0.80.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +45 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +11 -0
- package/corpus/core/src/agent/production-agent.ts +3 -0
- package/corpus/core/src/cli/plan-local.ts +445 -108
- package/corpus/core/src/cli/skills.ts +14 -5
- package/corpus/core/src/server/action-routes.ts +14 -0
- package/corpus/core/src/vite/client.ts +15 -1
- package/corpus/templates/clips/app/components/player/timestamped-comment-button.tsx +162 -0
- package/corpus/templates/clips/app/i18n/ar-SA.ts +4 -0
- package/corpus/templates/clips/app/i18n/de-DE.ts +4 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +4 -0
- package/corpus/templates/clips/app/i18n/es-ES.ts +4 -0
- package/corpus/templates/clips/app/i18n/fr-FR.ts +4 -0
- package/corpus/templates/clips/app/i18n/hi-IN.ts +4 -0
- package/corpus/templates/clips/app/i18n/ja-JP.ts +4 -0
- package/corpus/templates/clips/app/i18n/ko-KR.ts +4 -0
- package/corpus/templates/clips/app/i18n/pt-BR.ts +4 -0
- package/corpus/templates/clips/app/i18n/zh-CN.ts +4 -0
- package/corpus/templates/clips/app/i18n/zh-TW.ts +4 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +42 -18
- package/corpus/templates/plan/.agents/skills/visual-plan/references/local-files.md +14 -5
- package/corpus/templates/plan/.agents/skills/visual-recap/references/local-files.md +14 -5
- package/corpus/templates/plan/actions/validate-local-plan-source.ts +118 -0
- package/corpus/templates/plan/server/lib/plan-connector-catalog.ts +1 -0
- package/corpus/templates/plan/server/lib/public-action-paths.ts +1 -0
- package/dist/action.d.ts +8 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +3 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +3 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/plan-local.d.ts +30 -0
- package/dist/cli/plan-local.d.ts.map +1 -1
- package/dist/cli/plan-local.js +281 -56
- package/dist/cli/plan-local.js.map +1 -1
- package/dist/cli/skills.d.ts +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +14 -5
- package/dist/cli/skills.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +14 -0
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +12 -1
- package/dist/vite/client.js.map +1 -1
- package/package.json +1 -1
|
@@ -1405,8 +1405,10 @@ The local-files contract:
|
|
|
1405
1405
|
\`id\` and \`label\`; and \`Code\` / \`AnnotatedCode\` / \`Diff\` are whitespace-sensitive
|
|
1406
1406
|
— encode multiline code as JSON string attributes such as \`code={"const x =\\n y"}\`
|
|
1407
1407
|
(a static template literal is accepted only when it has no \`\${...}\`
|
|
1408
|
-
interpolation). \`plan local check\`
|
|
1409
|
-
renderer schema
|
|
1408
|
+
interpolation). \`plan local check\` is a quick OFFLINE lint (a subset of the
|
|
1409
|
+
renderer schema), so a green \`check\` does not guarantee the plan renders;
|
|
1410
|
+
\`plan local verify\` is the authoritative validation against the real renderer
|
|
1411
|
+
schema.
|
|
1410
1412
|
- **Write a local MDX folder.** Use \`plans/<slug>/\` to check the artifact into the
|
|
1411
1413
|
repo, or a repo-ignored/temporary folder such as \`.agent-native/plans/<slug>/\`
|
|
1412
1414
|
or \`/tmp/agent-native-plans/<slug>/\` when it should not be checked in. The
|
|
@@ -1431,9 +1433,16 @@ The local-files contract:
|
|
|
1431
1433
|
running local Plan app.
|
|
1432
1434
|
- **Headless verify.** Run
|
|
1433
1435
|
\`npx @agent-native/core@latest plan local verify --dir <plan-dir> --kind <plan|recap>\`.
|
|
1434
|
-
It starts the bridge, checks the private-network preflight and JSON payload,
|
|
1435
|
-
|
|
1436
|
-
\`
|
|
1436
|
+
It starts the bridge, checks the private-network preflight and JSON payload, AND
|
|
1437
|
+
validates the content against the real renderer schema via the Plan app's
|
|
1438
|
+
\`validate-local-plan-source\` action. A non-\`ok\` result with
|
|
1439
|
+
\`validation.valid: false\` lists the renderer's exact schema-path issues (e.g.
|
|
1440
|
+
\`blocks[1].data.tabs[0]...\`); fix those before handing off. If \`validation.ran\`
|
|
1441
|
+
is \`false\`, the Plan app did not expose the validate endpoint (older/unreachable
|
|
1442
|
+
deploy) — point \`--app-url\` at a current Plan app (e.g. a local
|
|
1443
|
+
\`http://localhost:8096\`) for the authoritative check. If the browser hangs on
|
|
1444
|
+
"Loading plan", fetch the \`bridgeUrl\` from the verify/serve JSON to read the
|
|
1445
|
+
concrete validation error.
|
|
1437
1446
|
- **Never call hosted tools for that plan/recap.** Do not call
|
|
1438
1447
|
\`create-visual-plan\`, \`create-ui-plan\`, \`create-prototype-plan\`,
|
|
1439
1448
|
\`create-plan-design\`, \`create-visual-recap\`, \`create-visual-questions\`,
|
|
@@ -284,6 +284,20 @@ export function mountActionRoutes(
|
|
|
284
284
|
return runWithRequestContext(
|
|
285
285
|
{ userEmail, userName, orgId, timezone },
|
|
286
286
|
async () => {
|
|
287
|
+
// Reject oversize bodies from Content-Length before parsing, so a
|
|
288
|
+
// public no-auth POST can't force parse work on a huge request.
|
|
289
|
+
if (typeof entry.maxBodyBytes === "number" && method !== "GET") {
|
|
290
|
+
const clRaw = getHeader(event, "content-length");
|
|
291
|
+
if (clRaw) {
|
|
292
|
+
const declared = parseInt(clRaw, 10);
|
|
293
|
+
if (!Number.isNaN(declared) && declared > entry.maxBodyBytes) {
|
|
294
|
+
setResponseStatus(event, 413);
|
|
295
|
+
return {
|
|
296
|
+
error: `Request body too large (max ${entry.maxBodyBytes} bytes)`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
287
301
|
// Parse params based on method. On web-standard runtimes (Netlify
|
|
288
302
|
// Functions, CF Workers), event.req IS the web Request — use .json()
|
|
289
303
|
// directly. H3's readBody fails on those runtimes because it expects
|
|
@@ -1087,7 +1087,21 @@ function baseRedirectGuard(): Plugin {
|
|
|
1087
1087
|
if (serveMountedEmbedRuntimeModule(server, req, res, base)) {
|
|
1088
1088
|
return;
|
|
1089
1089
|
}
|
|
1090
|
-
|
|
1090
|
+
// Nitro's pre-middleware only intercepts document/iframe/frame/empty
|
|
1091
|
+
// fetch-dest requests. For video/audio/image etc. it calls next() and
|
|
1092
|
+
// the post-internal Nitro middleware handles them instead. If we strip
|
|
1093
|
+
// the base path here for those requests, Vite's base middleware sees the
|
|
1094
|
+
// stripped path (e.g. /api/video/:id without /clips/) and responds with
|
|
1095
|
+
// a "did you mean /clips/api/video/:id" error before Nitro can handle it.
|
|
1096
|
+
// Only strip when the request type matches Nitro's pre-middleware gate.
|
|
1097
|
+
const secFetchDest = req.headers["sec-fetch-dest"] as
|
|
1098
|
+
| string
|
|
1099
|
+
| undefined;
|
|
1100
|
+
const isNitroPreHandled =
|
|
1101
|
+
!secFetchDest || /^(document|iframe|frame|empty)$/.test(secFetchDest);
|
|
1102
|
+
if (isNitroPreHandled) {
|
|
1103
|
+
req.url = stripMountedDevApiPath(req.url, base);
|
|
1104
|
+
}
|
|
1091
1105
|
if (
|
|
1092
1106
|
req.method === "HEAD" &&
|
|
1093
1107
|
req.url &&
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useActionMutation, useT } from "@agent-native/core/client";
|
|
2
|
+
import { IconMessagePlus, IconAt, IconMoodSmile } from "@tabler/icons-react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
import { msToClock } from "./scrubber";
|
|
10
|
+
|
|
11
|
+
interface TimestampedCommentButtonProps {
|
|
12
|
+
enableComments: boolean;
|
|
13
|
+
onOpen: () => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Trigger that opens the docked comment composer, pinned to the current time. */
|
|
18
|
+
export function TimestampedCommentButton({
|
|
19
|
+
enableComments,
|
|
20
|
+
onOpen,
|
|
21
|
+
className,
|
|
22
|
+
}: TimestampedCommentButtonProps) {
|
|
23
|
+
const t = useT();
|
|
24
|
+
if (!enableComments) return null;
|
|
25
|
+
return (
|
|
26
|
+
<Button
|
|
27
|
+
type="button"
|
|
28
|
+
variant="outline"
|
|
29
|
+
size="sm"
|
|
30
|
+
className={cn("gap-1.5", className)}
|
|
31
|
+
onClick={onOpen}
|
|
32
|
+
>
|
|
33
|
+
<IconMessagePlus className="h-4 w-4" />
|
|
34
|
+
{t("commentsPanel.commentButton")}
|
|
35
|
+
</Button>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface TimestampedCommentBarProps {
|
|
40
|
+
recordingId: string;
|
|
41
|
+
atMs: number;
|
|
42
|
+
onClose: () => void;
|
|
43
|
+
onAdded?: () => void;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Bottom-docked comment composer. Render inside a `relative` container (the
|
|
49
|
+
* video wrapper) so it overlays the bottom of the video at the captured moment.
|
|
50
|
+
*/
|
|
51
|
+
export function TimestampedCommentBar({
|
|
52
|
+
recordingId,
|
|
53
|
+
atMs,
|
|
54
|
+
onClose,
|
|
55
|
+
onAdded,
|
|
56
|
+
className,
|
|
57
|
+
}: TimestampedCommentBarProps) {
|
|
58
|
+
const t = useT();
|
|
59
|
+
const [draft, setDraft] = useState("");
|
|
60
|
+
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
61
|
+
|
|
62
|
+
const addComment = useActionMutation("add-comment");
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
requestAnimationFrame(() => textareaRef.current?.focus());
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const insertAtCursor = (text: string) => {
|
|
69
|
+
const el = textareaRef.current;
|
|
70
|
+
if (!el) {
|
|
71
|
+
setDraft((d) => d + text);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const start = el.selectionStart ?? draft.length;
|
|
75
|
+
const end = el.selectionEnd ?? draft.length;
|
|
76
|
+
const next = draft.slice(0, start) + text + draft.slice(end);
|
|
77
|
+
setDraft(next);
|
|
78
|
+
requestAnimationFrame(() => {
|
|
79
|
+
el.focus();
|
|
80
|
+
const pos = start + text.length;
|
|
81
|
+
el.setSelectionRange(pos, pos);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const submit = () => {
|
|
86
|
+
const content = draft.trim();
|
|
87
|
+
if (!content) return;
|
|
88
|
+
addComment.mutate(
|
|
89
|
+
{ recordingId, content, videoTimestampMs: atMs },
|
|
90
|
+
{
|
|
91
|
+
onSuccess: () => {
|
|
92
|
+
setDraft("");
|
|
93
|
+
onAdded?.();
|
|
94
|
+
onClose();
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className={cn("absolute inset-x-0 bottom-0 z-30 p-3", className)}>
|
|
102
|
+
<div className="rounded-xl border border-border bg-background/95 p-3 shadow-lg backdrop-blur">
|
|
103
|
+
<Textarea
|
|
104
|
+
ref={textareaRef}
|
|
105
|
+
value={draft}
|
|
106
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
107
|
+
onKeyDown={(e) => {
|
|
108
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
submit();
|
|
111
|
+
}
|
|
112
|
+
if (e.key === "Escape") {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
onClose();
|
|
115
|
+
}
|
|
116
|
+
}}
|
|
117
|
+
placeholder={t("commentsPanel.composerPlaceholder")}
|
|
118
|
+
rows={2}
|
|
119
|
+
className="min-h-[3rem] resize-none border-0 bg-transparent px-1 text-sm shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
|
120
|
+
/>
|
|
121
|
+
<div className="mt-2 flex items-center justify-between">
|
|
122
|
+
<div className="flex items-center gap-1">
|
|
123
|
+
<Button
|
|
124
|
+
type="button"
|
|
125
|
+
variant="ghost"
|
|
126
|
+
size="icon"
|
|
127
|
+
className="h-8 w-8 text-muted-foreground"
|
|
128
|
+
aria-label={t("commentsPanel.mentionSomeone")}
|
|
129
|
+
onClick={() => insertAtCursor("@")}
|
|
130
|
+
>
|
|
131
|
+
<IconAt className="h-4 w-4" />
|
|
132
|
+
</Button>
|
|
133
|
+
<Button
|
|
134
|
+
type="button"
|
|
135
|
+
variant="ghost"
|
|
136
|
+
size="icon"
|
|
137
|
+
className="h-8 w-8 text-muted-foreground"
|
|
138
|
+
aria-label={t("commentsPanel.addEmoji")}
|
|
139
|
+
onClick={() => insertAtCursor("🙂")}
|
|
140
|
+
>
|
|
141
|
+
<IconMoodSmile className="h-4 w-4" />
|
|
142
|
+
</Button>
|
|
143
|
+
</div>
|
|
144
|
+
<div className="flex items-center gap-2">
|
|
145
|
+
<Button type="button" variant="ghost" size="sm" onClick={onClose}>
|
|
146
|
+
{t("common.cancel")}
|
|
147
|
+
</Button>
|
|
148
|
+
<Button
|
|
149
|
+
type="button"
|
|
150
|
+
size="sm"
|
|
151
|
+
className="rounded-full"
|
|
152
|
+
disabled={!draft.trim() || addComment.isPending}
|
|
153
|
+
onClick={submit}
|
|
154
|
+
>
|
|
155
|
+
{t("commentsPanel.commentAt")} {msToClock(atMs)}
|
|
156
|
+
</Button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -850,6 +850,10 @@ const messages = {
|
|
|
850
850
|
replyingTo: "الرد على",
|
|
851
851
|
commentAt: "تعليق عند",
|
|
852
852
|
writeReply: "اكتب ردًا...",
|
|
853
|
+
commentButton: "تعليق",
|
|
854
|
+
composerPlaceholder: "أضف تعليقًا…",
|
|
855
|
+
mentionSomeone: "أذكر شخصًا",
|
|
856
|
+
addEmoji: "أضف رمزًا تعبيريًا",
|
|
853
857
|
},
|
|
854
858
|
shareMeeting: {
|
|
855
859
|
pageTitle: "ملاحظات الاجتماع · Clips",
|
|
@@ -878,6 +878,10 @@ Alle sichtbaren Änderungen für Clips-Nutzer werden hier dokumentiert. Du kanns
|
|
|
878
878
|
replyingTo: "Antwort an",
|
|
879
879
|
commentAt: "Kommentar bei",
|
|
880
880
|
writeReply: "Antwort schreiben...",
|
|
881
|
+
commentButton: "Kommentar",
|
|
882
|
+
composerPlaceholder: "Kommentar hinzufügen…",
|
|
883
|
+
mentionSomeone: "Jemanden erwähnen",
|
|
884
|
+
addEmoji: "Emoji hinzufügen",
|
|
881
885
|
},
|
|
882
886
|
shareMeeting: {
|
|
883
887
|
pageTitle: "Meeting-Notizen · Clips",
|
|
@@ -849,6 +849,10 @@ All notable user-facing changes to Clips are documented here. Open it any time f
|
|
|
849
849
|
replyingTo: "Replying to",
|
|
850
850
|
commentAt: "Comment at",
|
|
851
851
|
writeReply: "Write a reply...",
|
|
852
|
+
commentButton: "Comment",
|
|
853
|
+
composerPlaceholder: "Add a comment…",
|
|
854
|
+
mentionSomeone: "Mention someone",
|
|
855
|
+
addEmoji: "Add emoji",
|
|
852
856
|
},
|
|
853
857
|
shareMeeting: {
|
|
854
858
|
pageTitle: "Meeting notes · Clips",
|
|
@@ -870,6 +870,10 @@ Todos los cambios visibles para los usuarios de Clips se documentan aquí. Puede
|
|
|
870
870
|
replyingTo: "Respondiendo a",
|
|
871
871
|
commentAt: "Comentar en",
|
|
872
872
|
writeReply: "Escribe una respuesta...",
|
|
873
|
+
commentButton: "Comentar",
|
|
874
|
+
composerPlaceholder: "Añadir un comentario…",
|
|
875
|
+
mentionSomeone: "Mencionar a alguien",
|
|
876
|
+
addEmoji: "Añadir emoji",
|
|
873
877
|
},
|
|
874
878
|
shareMeeting: {
|
|
875
879
|
pageTitle: "Notas de reunión · Clips",
|
|
@@ -872,6 +872,10 @@ Tous les changements visibles par les utilisateurs de Clips sont documentés ici
|
|
|
872
872
|
replyingTo: "Réponse à",
|
|
873
873
|
commentAt: "Commentaire à",
|
|
874
874
|
writeReply: "Écrire une réponse...",
|
|
875
|
+
commentButton: "Commenter",
|
|
876
|
+
composerPlaceholder: "Ajouter un commentaire…",
|
|
877
|
+
mentionSomeone: "Mentionner quelqu'un",
|
|
878
|
+
addEmoji: "Ajouter un emoji",
|
|
875
879
|
},
|
|
876
880
|
shareMeeting: {
|
|
877
881
|
pageTitle: "Notes de réunion · Clips",
|
|
@@ -844,6 +844,10 @@ Clips में उपयोगकर्ताओं को दिखने व
|
|
|
844
844
|
replyingTo: "जवाब दे रहे हैं",
|
|
845
845
|
commentAt: "टिप्पणी करें",
|
|
846
846
|
writeReply: "जवाब लिखें...",
|
|
847
|
+
commentButton: "टिप्पणी",
|
|
848
|
+
composerPlaceholder: "टिप्पणी जोड़ें…",
|
|
849
|
+
mentionSomeone: "किसी का उल्लेख करें",
|
|
850
|
+
addEmoji: "इमोजी जोड़ें",
|
|
847
851
|
},
|
|
848
852
|
shareMeeting: {
|
|
849
853
|
pageTitle: "मीटिंग नोट्स · Clips",
|
|
@@ -860,6 +860,10 @@ Clips のユーザー向けの主な変更はここに記録されます。コ
|
|
|
860
860
|
replyingTo: "返信先",
|
|
861
861
|
commentAt: "コメント位置",
|
|
862
862
|
writeReply: "返信を書く...",
|
|
863
|
+
commentButton: "コメント",
|
|
864
|
+
composerPlaceholder: "コメントを追加…",
|
|
865
|
+
mentionSomeone: "メンバーをメンション",
|
|
866
|
+
addEmoji: "絵文字を追加",
|
|
863
867
|
},
|
|
864
868
|
shareMeeting: {
|
|
865
869
|
pageTitle: "会議メモ · Clips",
|
|
@@ -851,6 +851,10 @@ Clips의 모든 사용자 대상 변경 사항은 여기에 기록됩니다. 명
|
|
|
851
851
|
replyingTo: "답글 대상",
|
|
852
852
|
commentAt: "댓글 위치",
|
|
853
853
|
writeReply: "답글 쓰기...",
|
|
854
|
+
commentButton: "댓글",
|
|
855
|
+
composerPlaceholder: "댓글 추가…",
|
|
856
|
+
mentionSomeone: "멘션하기",
|
|
857
|
+
addEmoji: "이모지 추가",
|
|
854
858
|
},
|
|
855
859
|
shareMeeting: {
|
|
856
860
|
pageTitle: "회의 노트 · Clips",
|
|
@@ -867,6 +867,10 @@ Todas as mudanças visíveis para usuários do Clips são documentadas aqui. Voc
|
|
|
867
867
|
replyingTo: "Respondendo a",
|
|
868
868
|
commentAt: "Comentário em",
|
|
869
869
|
writeReply: "Escreva uma resposta...",
|
|
870
|
+
commentButton: "Comentar",
|
|
871
|
+
composerPlaceholder: "Adicionar um comentário…",
|
|
872
|
+
mentionSomeone: "Mencionar alguém",
|
|
873
|
+
addEmoji: "Adicionar emoji",
|
|
870
874
|
},
|
|
871
875
|
shareMeeting: {
|
|
872
876
|
pageTitle: "Notas da reunião · Clips",
|
|
@@ -817,6 +817,10 @@ Clips 中所有面向用户的重要更改都会记录在这里。你可以随
|
|
|
817
817
|
replyingTo: "正在回复",
|
|
818
818
|
commentAt: "评论于",
|
|
819
819
|
writeReply: "写回复...",
|
|
820
|
+
commentButton: "评论",
|
|
821
|
+
composerPlaceholder: "添加评论…",
|
|
822
|
+
mentionSomeone: "提及某人",
|
|
823
|
+
addEmoji: "添加表情",
|
|
820
824
|
},
|
|
821
825
|
shareMeeting: {
|
|
822
826
|
pageTitle: "会议笔记 · Clips",
|
|
@@ -810,6 +810,10 @@ const messages = {
|
|
|
810
810
|
replyingTo: "正在回覆",
|
|
811
811
|
commentAt: "評論於",
|
|
812
812
|
writeReply: "寫回覆...",
|
|
813
|
+
commentButton: "留言",
|
|
814
|
+
composerPlaceholder: "新增留言…",
|
|
815
|
+
mentionSomeone: "提及某人",
|
|
816
|
+
addEmoji: "新增表情符號",
|
|
813
817
|
},
|
|
814
818
|
shareMeeting: {
|
|
815
819
|
pageTitle: "會議筆記 · Clips",
|
|
@@ -44,6 +44,10 @@ import { InsightsPanel } from "@/components/player/insights-panel";
|
|
|
44
44
|
import { ReactionsTray } from "@/components/player/reactions-tray";
|
|
45
45
|
import { SettingsPanel } from "@/components/player/settings-panel";
|
|
46
46
|
import { ShareRecordingPopover } from "@/components/player/share-dialog";
|
|
47
|
+
import {
|
|
48
|
+
TimestampedCommentButton,
|
|
49
|
+
TimestampedCommentBar,
|
|
50
|
+
} from "@/components/player/timestamped-comment-button";
|
|
47
51
|
import { TranscriptPanel } from "@/components/player/transcript-panel";
|
|
48
52
|
import {
|
|
49
53
|
VideoPlayer,
|
|
@@ -210,7 +214,25 @@ export default function RecordingPage() {
|
|
|
210
214
|
const [theaterMode, setTheaterMode] = useState(false);
|
|
211
215
|
const [editing, setEditing] = useState(false);
|
|
212
216
|
const [currentMs, setCurrentMs] = useState(0);
|
|
217
|
+
const [commentOpen, setCommentOpen] = useState(false);
|
|
218
|
+
const [commentAtMs, setCommentAtMs] = useState(0);
|
|
213
219
|
const isCompactLayout = useIsCompactRecordingLayout();
|
|
220
|
+
// Resolve the playback position for reactions/comments. Native <video> exposes
|
|
221
|
+
// a live `currentTime`; Loom embeds render in a cross-origin iframe with no
|
|
222
|
+
// live time bridge, so we fall back to the last position the player reported
|
|
223
|
+
// via onTimeUpdate (seek/initial start).
|
|
224
|
+
const resolvePlaybackMs = useCallback(() => {
|
|
225
|
+
const liveCt = playerRef.current?.video?.currentTime;
|
|
226
|
+
if (
|
|
227
|
+
typeof liveCt === "number" &&
|
|
228
|
+
Number.isFinite(liveCt) &&
|
|
229
|
+
liveCt >= 0 &&
|
|
230
|
+
liveCt < 1e7
|
|
231
|
+
) {
|
|
232
|
+
return Math.floor(liveCt * 1000);
|
|
233
|
+
}
|
|
234
|
+
return currentMs;
|
|
235
|
+
}, [currentMs]);
|
|
214
236
|
const transcriptKickedRef = useRef<string | null>(null);
|
|
215
237
|
// When the recording lands in the processing state but never flips to
|
|
216
238
|
// 'ready', stop spinning forever and surface an error banner so the user
|
|
@@ -1082,7 +1104,7 @@ export default function RecordingPage() {
|
|
|
1082
1104
|
<EditorLayout recordingId={recording.id} className="flex-1" />
|
|
1083
1105
|
) : (
|
|
1084
1106
|
<>
|
|
1085
|
-
<div className="flex-1 min-h-0">
|
|
1107
|
+
<div className="flex-1 min-h-0 relative">
|
|
1086
1108
|
<VideoPlayer
|
|
1087
1109
|
ref={playerRef}
|
|
1088
1110
|
recordingId={recording.id}
|
|
@@ -1107,6 +1129,18 @@ export default function RecordingPage() {
|
|
|
1107
1129
|
onTimeUpdate={(ms) => setCurrentMs(ms)}
|
|
1108
1130
|
className="h-full"
|
|
1109
1131
|
/>
|
|
1132
|
+
{commentOpen ? (
|
|
1133
|
+
<TimestampedCommentBar
|
|
1134
|
+
recordingId={recording.id}
|
|
1135
|
+
atMs={commentAtMs}
|
|
1136
|
+
onClose={() => setCommentOpen(false)}
|
|
1137
|
+
onAdded={() => {
|
|
1138
|
+
setPanel("comments");
|
|
1139
|
+
if (isCompactLayout) setSidePanelOpen(true);
|
|
1140
|
+
void playerDataQ.refetch();
|
|
1141
|
+
}}
|
|
1142
|
+
/>
|
|
1143
|
+
) : null}
|
|
1110
1144
|
</div>
|
|
1111
1145
|
|
|
1112
1146
|
{/* Title + reactions row */}
|
|
@@ -1129,15 +1163,12 @@ export default function RecordingPage() {
|
|
|
1129
1163
|
</span>
|
|
1130
1164
|
</NavLink>
|
|
1131
1165
|
) : null}
|
|
1132
|
-
<
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
className="text-base font-semibold"
|
|
1139
|
-
inputClassName="h-8 text-base font-semibold"
|
|
1140
|
-
skeletonClassName="h-5 w-72 max-w-full"
|
|
1166
|
+
<TimestampedCommentButton
|
|
1167
|
+
enableComments={recording.enableComments}
|
|
1168
|
+
onOpen={() => {
|
|
1169
|
+
setCommentAtMs(resolvePlaybackMs());
|
|
1170
|
+
setCommentOpen(true);
|
|
1171
|
+
}}
|
|
1141
1172
|
/>
|
|
1142
1173
|
{recording.description ? (
|
|
1143
1174
|
<p className="text-sm text-muted-foreground line-clamp-2">
|
|
@@ -1150,14 +1181,7 @@ export default function RecordingPage() {
|
|
|
1150
1181
|
disabled={!recording.enableReactions}
|
|
1151
1182
|
onReact={(emoji) => {
|
|
1152
1183
|
tracking.reportReaction(emoji);
|
|
1153
|
-
const
|
|
1154
|
-
const liveMs =
|
|
1155
|
-
typeof liveCt === "number" &&
|
|
1156
|
-
Number.isFinite(liveCt) &&
|
|
1157
|
-
liveCt >= 0 &&
|
|
1158
|
-
liveCt < 1e7
|
|
1159
|
-
? Math.floor(liveCt * 1000)
|
|
1160
|
-
: currentMs;
|
|
1184
|
+
const liveMs = resolvePlaybackMs();
|
|
1161
1185
|
fetch(
|
|
1162
1186
|
agentNativePath(
|
|
1163
1187
|
"/_agent-native/actions/react-to-recording",
|
|
@@ -35,8 +35,10 @@ The local-files contract:
|
|
|
35
35
|
`id` and `label`; and `Code` / `AnnotatedCode` / `Diff` are whitespace-sensitive
|
|
36
36
|
— encode multiline code as JSON string attributes such as `code={"const x =\n y"}`
|
|
37
37
|
(a static template literal is accepted only when it has no `${...}`
|
|
38
|
-
interpolation). `plan local check`
|
|
39
|
-
renderer schema
|
|
38
|
+
interpolation). `plan local check` is a quick OFFLINE lint (a subset of the
|
|
39
|
+
renderer schema), so a green `check` does not guarantee the plan renders;
|
|
40
|
+
`plan local verify` is the authoritative validation against the real renderer
|
|
41
|
+
schema.
|
|
40
42
|
- **Write a local MDX folder.** Use `plans/<slug>/` to check the artifact into the
|
|
41
43
|
repo, or a repo-ignored/temporary folder such as `.agent-native/plans/<slug>/`
|
|
42
44
|
or `/tmp/agent-native-plans/<slug>/` when it should not be checked in. The
|
|
@@ -61,9 +63,16 @@ The local-files contract:
|
|
|
61
63
|
running local Plan app.
|
|
62
64
|
- **Headless verify.** Run
|
|
63
65
|
`npx @agent-native/core@latest plan local verify --dir <plan-dir> --kind <plan|recap>`.
|
|
64
|
-
It starts the bridge, checks the private-network preflight and JSON payload,
|
|
65
|
-
|
|
66
|
-
`
|
|
66
|
+
It starts the bridge, checks the private-network preflight and JSON payload, AND
|
|
67
|
+
validates the content against the real renderer schema via the Plan app's
|
|
68
|
+
`validate-local-plan-source` action. A non-`ok` result with
|
|
69
|
+
`validation.valid: false` lists the renderer's exact schema-path issues (e.g.
|
|
70
|
+
`blocks[1].data.tabs[0]...`); fix those before handing off. If `validation.ran`
|
|
71
|
+
is `false`, the Plan app did not expose the validate endpoint (older/unreachable
|
|
72
|
+
deploy) — point `--app-url` at a current Plan app (e.g. a local
|
|
73
|
+
`http://localhost:8096`) for the authoritative check. If the browser hangs on
|
|
74
|
+
"Loading plan", fetch the `bridgeUrl` from the verify/serve JSON to read the
|
|
75
|
+
concrete validation error.
|
|
67
76
|
- **Never call hosted tools for that plan/recap.** Do not call
|
|
68
77
|
`create-visual-plan`, `create-ui-plan`, `create-prototype-plan`,
|
|
69
78
|
`create-plan-design`, `create-visual-recap`, `create-visual-questions`,
|
|
@@ -35,8 +35,10 @@ The local-files contract:
|
|
|
35
35
|
`id` and `label`; and `Code` / `AnnotatedCode` / `Diff` are whitespace-sensitive
|
|
36
36
|
— encode multiline code as JSON string attributes such as `code={"const x =\n y"}`
|
|
37
37
|
(a static template literal is accepted only when it has no `${...}`
|
|
38
|
-
interpolation). `plan local check`
|
|
39
|
-
renderer schema
|
|
38
|
+
interpolation). `plan local check` is a quick OFFLINE lint (a subset of the
|
|
39
|
+
renderer schema), so a green `check` does not guarantee the plan renders;
|
|
40
|
+
`plan local verify` is the authoritative validation against the real renderer
|
|
41
|
+
schema.
|
|
40
42
|
- **Write a local MDX folder.** Use `plans/<slug>/` to check the artifact into the
|
|
41
43
|
repo, or a repo-ignored/temporary folder such as `.agent-native/plans/<slug>/`
|
|
42
44
|
or `/tmp/agent-native-plans/<slug>/` when it should not be checked in. The
|
|
@@ -61,9 +63,16 @@ The local-files contract:
|
|
|
61
63
|
running local Plan app.
|
|
62
64
|
- **Headless verify.** Run
|
|
63
65
|
`npx @agent-native/core@latest plan local verify --dir <plan-dir> --kind <plan|recap>`.
|
|
64
|
-
It starts the bridge, checks the private-network preflight and JSON payload,
|
|
65
|
-
|
|
66
|
-
`
|
|
66
|
+
It starts the bridge, checks the private-network preflight and JSON payload, AND
|
|
67
|
+
validates the content against the real renderer schema via the Plan app's
|
|
68
|
+
`validate-local-plan-source` action. A non-`ok` result with
|
|
69
|
+
`validation.valid: false` lists the renderer's exact schema-path issues (e.g.
|
|
70
|
+
`blocks[1].data.tabs[0]...`); fix those before handing off. If `validation.ran`
|
|
71
|
+
is `false`, the Plan app did not expose the validate endpoint (older/unreachable
|
|
72
|
+
deploy) — point `--app-url` at a current Plan app (e.g. a local
|
|
73
|
+
`http://localhost:8096`) for the authoritative check. If the browser hangs on
|
|
74
|
+
"Loading plan", fetch the `bridgeUrl` from the verify/serve JSON to read the
|
|
75
|
+
concrete validation error.
|
|
67
76
|
- **Never call hosted tools for that plan/recap.** Do not call
|
|
68
77
|
`create-visual-plan`, `create-ui-plan`, `create-prototype-plan`,
|
|
69
78
|
`create-plan-design`, `create-visual-recap`, `create-visual-questions`,
|