("value");
const [intent, setIntent] = useState("");
const [productId, setProductId] = useState("");
- const [accountId, setAccountId] = useState("");
const [goal, setGoal] = useState(8);
const [todayDone, setTodayDone] = useState(0);
@@ -253,20 +246,16 @@ export function ScoutPage() {
useEffect(() => {
void (async () => {
try {
- const [b, hw, prods, postList, acc] = await Promise.all([
+ const [b, hw, prods, postList] = await Promise.all([
repos.scout.listBrands(),
- repos.scout.listHomework(),
- repos.scout.listAllProducts(),
- repos.scout.listPosts(),
- repos.accounts.list(),
+ repos.scout.listHomework(),
+ repos.scout.listAllProducts(),
+ repos.scout.listPosts(),
]);
- setBrands(b);
- setHomeworkList(hw);
- setAllProducts(prods);
- setPosts(postList);
- const usable = acc.filter((a) => a.is_usable);
- setAccounts(usable);
- setAccountId((current) => current || usable[0]?.id || "");
+ setBrands(b);
+ setHomeworkList(hw);
+ setAllProducts(prods);
+ setPosts(postList);
const pending = postList.filter(isPending).sort((a, b) => (b.score || 0) - (a.score || 0));
if (pending[0]) setActiveRunKey((cur) => cur || postRunKey(pending[0]!));
@@ -591,32 +580,39 @@ export function ScoutPage() {
}
}
- async function sendCurrent() {
+ function openThreadsReply() {
if (!current) return;
- setBusy("send");
+ const url = allowHttpUrl(current.permalink);
+ if (!url) {
+ setMessage(t("scout.noPermalink"));
+ return;
+ }
+ window.open(url, "_blank", "noopener,noreferrer");
+ const text = draftText.trim();
+ if (text && navigator.clipboard?.writeText) {
+ void navigator.clipboard.writeText(text).then(
+ () => setMessage(t("scout.openedAndCopied")),
+ () => setMessage(t("scout.openedManual")),
+ );
+ } else {
+ setMessage(t("scout.openedManual"));
+ }
+ }
+
+ async function markManualDone() {
+ if (!current) return;
+ const id = current.id;
+ setBusy("manual-done");
setMessage("");
try {
- let text = draftText.trim();
- if (!text) {
- const d = await repos.scout.draftOutreach(current.id);
- text = (d.draft_text || "").trim();
- setDraftText(text);
- }
- if (!text) throw new Error(t("scout.noDraft"));
- const id = current.id;
- await repos.scout.sendOutreach({
- postId: id,
- text,
- accountId: accountId || undefined,
- });
+ await repos.scout.markPublished(id);
const list = await reloadPosts();
pickNext(id, list);
const today = bumpScoutTodayDone();
setTodayDone(today.done);
- const who = accounts.find((a) => a.id === accountId)?.username || t("scout.accountFallback");
- setMessage(t("scout.queued", { who, done: today.done, goal }));
+ setMessage(t("scout.manualDone", { done: today.done, goal }));
} catch (e) {
- setMessage(e instanceof Error ? e.message : t("scout.sendFail"));
+ setMessage(e instanceof Error ? e.message : t("scout.manualDoneFail"));
} finally {
setBusy("");
}
@@ -873,23 +869,9 @@ export function ScoutPage() {
placeholder={current.scout_mode === "activity" ? t("scout.draftPhActivity") : t("scout.draftPhValue")}
/>
-
-
-
+
+ {t("scout.manualReplyHint")}
+
- {accounts.length === 0 ? (
-
- {t("scout.needAccountBefore")}{" "}
- {t("nav.crew")}{" "}
- {t("scout.needAccountAfter")}
-
- ) : null}
)}
diff --git a/apps/web/src/pages/StudioPage.tsx b/apps/web/src/pages/StudioPage.tsx
index 6a303c4..e27da6d 100644
--- a/apps/web/src/pages/StudioPage.tsx
+++ b/apps/web/src/pages/StudioPage.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from "react";
+import { lazy, Suspense, useEffect, useMemo, useState } from "react";
import { Link, useSearchParams } from "react-router-dom";
import { PageHeader } from "../components/layout/PageHeader";
import { Select } from "../components/ui";
@@ -6,12 +6,13 @@ import { useData, useRepos } from "../data/DataContext";
import type { Persona, ThreadsAccount } from "../domain/types";
import { useI18n } from "../i18n/I18nContext";
import { isPersonaReady, personaOptionLabel } from "../lib/personaPrompt";
-import { ComposerPanel } from "./studio/ComposerPanel";
-import { InspirePanel } from "./studio/InspirePanel";
-import { InsightsPanel } from "./studio/InsightsPanel";
-import { MentionsPanel } from "./studio/MentionsPanel";
-import { OwnPostsPanel } from "./studio/OwnPostsPanel";
-import { PlaysPanel } from "./studio/PlaysPanel";
+
+const ComposerPanel = lazy(() => import("./studio/ComposerPanel").then((m) => ({ default: m.ComposerPanel })));
+const InspirePanel = lazy(() => import("./studio/InspirePanel").then((m) => ({ default: m.InspirePanel })));
+const InsightsPanel = lazy(() => import("./studio/InsightsPanel").then((m) => ({ default: m.InsightsPanel })));
+const MentionsPanel = lazy(() => import("./studio/MentionsPanel").then((m) => ({ default: m.MentionsPanel })));
+const OwnPostsPanel = lazy(() => import("./studio/OwnPostsPanel").then((m) => ({ default: m.OwnPostsPanel })));
+const PlaysPanel = lazy(() => import("./studio/PlaysPanel").then((m) => ({ default: m.PlaysPanel })));
export type StudioTab = "posts" | "mentions" | "compose" | "plays" | "inspire" | "insights";
@@ -130,34 +131,22 @@ export function StudioPage() {
))}
- {tab === "posts" ? (
-
- ) : null}
- {tab === "mentions" ? (
-
- ) : null}
- {tab === "compose" ? (
-
- ) : null}
- {tab === "plays" ? (
-
- ) : null}
- {tab === "inspire" ? (
-
- ) : null}
- {tab === "insights" ? (
-
- ) : null}
+ }>
+ {tab === "posts" ? (
+
+ ) : null}
+ {tab === "mentions" ? (
+
+ ) : null}
+ {tab === "compose" ? (
+
+ ) : null}
+ {tab === "plays" ? : null}
+ {tab === "inspire" ? (
+
+ ) : null}
+ {tab === "insights" ? : null}
+
>
);
}
diff --git a/apps/web/src/pages/TodayPage.tsx b/apps/web/src/pages/TodayPage.tsx
index 3a97320..b9a1313 100644
--- a/apps/web/src/pages/TodayPage.tsx
+++ b/apps/web/src/pages/TodayPage.tsx
@@ -73,11 +73,13 @@ export function TodayPage() {
const scoutLocal = loadScoutToday();
setScoutGoal(scoutLocal.goalValue);
- const [posts, box, trendList, acc] = await Promise.all([
+ const [posts, box, trendList, acc, owns, mentionList] = await Promise.all([
repos.scout.listPosts(),
repos.outbox.list(),
repos.inspiration.listTrends("all").catch(() => [] as TrendItem[]),
repos.accounts.list(),
+ repos.ownPosts.list().catch(() => [] as OwnPost[]),
+ repos.mentions.list().catch(() => [] as MentionItem[]),
]);
const usable = acc.filter((a) => a.is_usable);
@@ -89,6 +91,8 @@ export function TodayPage() {
.sort((a, b) => (b.heat || 0) - (a.heat || 0))
.slice(0, 12),
);
+ setOwnPosts(owns);
+ setMentions(mentionList);
// 舊資料沒有發佈時間時不可拿歷史 published 總數冒充今日完成量。
const dayStart = startOfLocalDayNano();
@@ -100,35 +104,16 @@ export function TodayPage() {
).length;
setScoutDone(Math.max(scoutLocal.done, publishedN));
- // 自己貼文:全帳或分帳合併
- let owns: OwnPost[] = [];
- try {
- owns = await repos.ownPosts.list();
- } catch {
- owns = [];
- }
+ // 舊 API 若不支援全帳列表,分帳 fallback 在背景補齊,不阻塞首屏。
if (!owns.length && usable.length) {
- const chunks = await Promise.all(
- usable.slice(0, 6).map((a) =>
- repos.ownPosts.list(a.id).catch(() => [] as OwnPost[]),
- ),
- );
- owns = chunks.flat();
+ void Promise.all(
+ usable.slice(0, 6).map((a) => repos.ownPosts.list(a.id).catch(() => [] as OwnPost[])),
+ ).then((chunks) => setOwnPosts(chunks.flat()));
}
- setOwnPosts(owns);
- // 待回提及
- let mentionList: MentionItem[] = [];
- try {
- if (usable[0]?.id) {
- mentionList = await repos.mentions.list(usable[0].id);
- } else {
- mentionList = await repos.mentions.list();
- }
- } catch {
- mentionList = [];
+ if (!mentionList.length && usable[0]?.id) {
+ void repos.mentions.list(usable[0].id).then(setMentions).catch(() => undefined);
}
- setMentions(mentionList);
} catch (e) {
setError(formatApiError(e, "today.loadFail"));
} finally {
diff --git a/apps/web/src/pages/studio/ComposerPanel.tsx b/apps/web/src/pages/studio/ComposerPanel.tsx
index c969d5c..51d9ff8 100644
--- a/apps/web/src/pages/studio/ComposerPanel.tsx
+++ b/apps/web/src/pages/studio/ComposerPanel.tsx
@@ -48,6 +48,7 @@ export function ComposerPanel({ accountId, personaId, personaReady }: Props) {
const [topicTag, setTopicTag] = useState("");
const [showMimic, setShowMimic] = useState(false);
const [sourceText, setSourceText] = useState("");
+ const [mimicDirection, setMimicDirection] = useState("");
/** 從「我的貼文」帶來的結構分析備註(仿寫時一併送後端) */
const [structureNotes, setStructureNotes] = useState("");
const [images, setImages] = useState([]);
@@ -57,6 +58,7 @@ export function ComposerPanel({ accountId, personaId, personaReady }: Props) {
const [scheduleImmediate, setScheduleImmediate] = useState(true);
const [busy, setBusy] = useState("");
const [message, setMessage] = useState("");
+ const bodyLength = [...text].length;
/** 進行中的仿寫 job id(session 保留,重整後仍可接回) */
const [mimicJobId, setMimicJobId] = useState(() => {
try {
@@ -183,7 +185,12 @@ export function ComposerPanel({ accountId, personaId, personaReady }: Props) {
setMessage("");
appliedMimicJob.current = "";
try {
- const out = await repos.compose.mimic(sourceText, personaId, structureNotes || undefined);
+ const out = await repos.compose.mimic(
+ sourceText,
+ personaId,
+ mimicDirection || undefined,
+ structureNotes || undefined,
+ );
if (out.async && out.jobId) {
trackMimicJob(out.jobId);
setMessage(t("compose.mimic.queued"));
@@ -294,6 +301,10 @@ export function ComposerPanel({ accountId, personaId, personaReady }: Props) {
placeholder={t("compose.bodyPh")}
rows={8}
/>
+ 500 ? "status" : undefined}>
+ {t("compose.bodyCount", { n: bodyLength })}
+ {bodyLength > 500 ? ` · ${t("compose.bodyLongWarning")}` : ""}
+
+