mirror of
https://github.com/alphaXiv/OpenResearch.git
synced 2026-10-02 01:34:34 +08:00
OR-280 Nudge new installs to run the demo experiment (#338)
* Nudge new installs to run the demo experiment Analytics for Sep 9-12 installs showed the largest funnel leak between "onboarding complete" and "first message sent" (43.8%). The demo welcome steered users to read the recorded sessions rather than send anything. - Rename the welcome's primary button to "Run the demo experiment" and hand focus to the composer, which already holds the run-experiment prompt. - Keep that prefill on offer until the user has sent anything in the demo, detected by a recorded session's active leaf moving off its seeded message or a new session appearing, instead of only until the welcome closes. - Show a dismissible hint above the composer explaining the 200-step probe, with a ring on the send button while the prefilled prompt is untouched. - Open only the experiments tab on the first demo visit. - Default the right pane to a quarter of the window (max 480px) on installs with no saved layout, and share that default with the projects home so it is not overwritten before the demo opens. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Refine the demo nudge and add a running-experiment hint - Relabel the welcome button "Try the demo experiment": the click only focuses the prefilled prompt, so "Run" over-promised. - Style the composer hint as a neutral surface bubble instead of a tint. - When a demo run starts, show a second dismissible hint above the composer with a Logs button that opens that run's logs; it clears when the run leaves the running state. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Address review on the running-experiment hint - Seed the running-run hint from the runs baseline so it survives a reload or a return to the demo project mid-run. - Announce the hint to assistive tech via a polite live region and return focus to the composer on dismiss, matching the prompt hint. - Share the hint bubble classes, drop the redundant Logs tooltip key, and use the idiomatic Arabic phrasing for the tap instruction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
470ab76cfa
commit
b38c8136e6
Vendored
-1
File diff suppressed because one or more lines are too long
+212
-212
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -49,8 +49,8 @@
|
||||
html { background: #ffffff; }
|
||||
html[data-theme="dark"] { background: #0e0c0c; }
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-mNCWAddR.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BJLMzvdf.css">
|
||||
<script type="module" crossorigin src="/assets/index-C4inFLcC.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CdfPFYKN.css">
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
|
||||
+5
-1
@@ -69,7 +69,11 @@
|
||||
"chat_panel_data_sources": "مصادر البيانات",
|
||||
"chat_panel_delegated_a_task_to_a_new_agent": "فوّض مهمة إلى وكيل جديد",
|
||||
"chat_panel_delete": "حذف",
|
||||
"chat_panel_demo_hint_body": "يشغّل هذا الطلب اختبار تدريب صغيرًا من 200 خطوة على جهازك. اضغط Enter للبدء.",
|
||||
"chat_panel_demo_run_hint_after": "لمتابعتها.",
|
||||
"chat_panel_demo_run_hint_before": "التجربة قيد التشغيل الآن. اضغط على",
|
||||
"chat_panel_deny": "رفض",
|
||||
"chat_panel_dismiss_demo_hint": "إغلاق",
|
||||
"chat_panel_edit_and_re_send": "تعديل وإعادة الإرسال",
|
||||
"chat_panel_edit_message": "تعديل الرسالة",
|
||||
"chat_panel_edited_a_file": "عدّل ملفًا",
|
||||
@@ -474,6 +478,7 @@
|
||||
"tour_demo_project": "مشروع توضيحي",
|
||||
"tour_look_through_the_agent_conversations_experiments_runs_and": "تصفّح محادثات الوكيل والتجارب وعمليات التشغيل والمُخرَجات لترى كيف يتشكّل مشروع على OpenResearch.",
|
||||
"tour_nanochat": "nanochat",
|
||||
"tour_run_demo_experiment": "جرّب التجربة التوضيحية",
|
||||
"tour_this_is_a_demo_project_showing_how_open": "هذا مشروع توضيحي يبيّن كيف يعمل OpenResearch. يستخدم هذا العرض مستودع",
|
||||
"tour_welcome_to_open_research": "مرحبًا بك في OpenResearch",
|
||||
"tree_view_ask_in_this_task_to_create_one_or": "اطلب في هذه المهمة إنشاء واحدة، أو بدّل إلى «المشروع بالكامل» لرؤية جميع التجارب.",
|
||||
@@ -751,7 +756,6 @@
|
||||
"artifacts_too_large_to_preview": "الملف كبير جدًا للمعاينة المضمّنة.",
|
||||
"artifacts_open_raw": "فتح الملف الخام",
|
||||
"artifacts_copy_path": "نسخ المسار",
|
||||
"tour_explore_demo": "استكشاف العرض التوضيحي",
|
||||
"experiment_latest_run": "أحدث تشغيل",
|
||||
"experiment_runs": "عمليات التشغيل",
|
||||
"experiments_none_yet": "لا توجد تجارب بعد.",
|
||||
|
||||
+5
-1
@@ -69,7 +69,11 @@
|
||||
"chat_panel_data_sources": "Data sources",
|
||||
"chat_panel_delegated_a_task_to_a_new_agent": "Delegated a task to a new agent",
|
||||
"chat_panel_delete": "Delete",
|
||||
"chat_panel_demo_hint_body": "This prompt runs a small 200-step training probe on your machine. Hit enter to get started.",
|
||||
"chat_panel_demo_run_hint_after": "to monitor it.",
|
||||
"chat_panel_demo_run_hint_before": "The experiment is now running. Tap",
|
||||
"chat_panel_deny": "Deny",
|
||||
"chat_panel_dismiss_demo_hint": "Dismiss",
|
||||
"chat_panel_edit_and_re_send": "Edit and re-send",
|
||||
"chat_panel_edit_message": "Edit message",
|
||||
"chat_panel_edited_a_file": "Edited a file",
|
||||
@@ -474,6 +478,7 @@
|
||||
"tour_demo_project": "Demo project",
|
||||
"tour_look_through_the_agent_conversations_experiments_runs_and": "Look through the agent conversations, experiments, runs, and artifacts to see how a project on OpenResearch comes together.",
|
||||
"tour_nanochat": "nanochat",
|
||||
"tour_run_demo_experiment": "Try the demo experiment",
|
||||
"tour_this_is_a_demo_project_showing_how_open": "This is a demo project showing how OpenResearch works. This demo uses Andrej Karpathy's",
|
||||
"tour_welcome_to_open_research": "Welcome to OpenResearch",
|
||||
"tree_view_ask_in_this_task_to_create_one_or": "Ask in this task to create one, or switch to Entire project to see all experiments.",
|
||||
@@ -751,7 +756,6 @@
|
||||
"artifacts_too_large_to_preview": "File too large to preview inline.",
|
||||
"artifacts_open_raw": "Open raw",
|
||||
"artifacts_copy_path": "Copy path",
|
||||
"tour_explore_demo": "Explore the demo",
|
||||
"experiment_latest_run": "Latest run",
|
||||
"experiment_runs": "Runs",
|
||||
"experiments_none_yet": "No experiments yet.",
|
||||
|
||||
+5
-1
@@ -69,7 +69,11 @@
|
||||
"chat_panel_data_sources": "منابع داده",
|
||||
"chat_panel_delegated_a_task_to_a_new_agent": "وظیفه به عامل جدید واگذار شد",
|
||||
"chat_panel_delete": "حذف",
|
||||
"chat_panel_demo_hint_body": "این پیام یک کاوش آموزشی کوچک ۲۰۰ مرحلهای روی دستگاه شما اجرا میکند. برای شروع Enter را بزنید.",
|
||||
"chat_panel_demo_run_hint_after": "بزنید.",
|
||||
"chat_panel_demo_run_hint_before": "آزمایش اکنون در حال اجراست. برای پیگیری روی",
|
||||
"chat_panel_deny": "رد کردن",
|
||||
"chat_panel_dismiss_demo_hint": "بستن",
|
||||
"chat_panel_edit_and_re_send": "ویرایش و ارسال دوباره",
|
||||
"chat_panel_edit_message": "ویرایش پیام",
|
||||
"chat_panel_edited_a_file": "فایل ویرایش شد",
|
||||
@@ -470,6 +474,7 @@
|
||||
"tour_demo_project": "پروژهٔ نمایشی",
|
||||
"tour_look_through_the_agent_conversations_experiments_runs_and": "گفتگوهای عامل، آزمایشها، اجراها و خروجیها را ببینید تا با شکلگیری یک پروژه در OpenResearch آشنا شوید.",
|
||||
"tour_nanochat": "nanochat",
|
||||
"tour_run_demo_experiment": "آزمایش نمایشی را امتحان کنید",
|
||||
"tour_this_is_a_demo_project_showing_how_open": "این پروژهٔ نمایشی نحوهٔ کار OpenResearch را نشان میدهد. این نسخهٔ نمایشی از",
|
||||
"tour_welcome_to_open_research": "به OpenResearch خوش آمدید",
|
||||
"tree_view_ask_in_this_task_to_create_one_or": "در این وظیفه بخواهید یکی ساخته شود، یا برای دیدن همهٔ آزمایشها به «کل پروژه» بروید.",
|
||||
@@ -745,7 +750,6 @@
|
||||
"artifacts_too_large_to_preview": "فایل برای پیشنمایش درونصفحهای بیش از حد بزرگ است.",
|
||||
"artifacts_open_raw": "باز کردن فایل خام",
|
||||
"artifacts_copy_path": "کپی مسیر",
|
||||
"tour_explore_demo": "دیدن پروژهٔ نمایشی",
|
||||
"experiment_latest_run": "آخرین اجرا",
|
||||
"experiment_runs": "اجراها",
|
||||
"experiments_none_yet": "هنوز آزمایشی وجود ندارد.",
|
||||
|
||||
@@ -69,7 +69,11 @@
|
||||
"chat_panel_data_sources": "数据源",
|
||||
"chat_panel_delegated_a_task_to_a_new_agent": "已将任务委派给新智能体",
|
||||
"chat_panel_delete": "删除",
|
||||
"chat_panel_demo_hint_body": "这条提示会在你的机器上运行一个小型 200 步训练探测。按 Enter 开始。",
|
||||
"chat_panel_demo_run_hint_after": "以查看进度。",
|
||||
"chat_panel_demo_run_hint_before": "实验正在运行。点按",
|
||||
"chat_panel_deny": "拒绝",
|
||||
"chat_panel_dismiss_demo_hint": "关闭",
|
||||
"chat_panel_edit_and_re_send": "编辑并重新发送",
|
||||
"chat_panel_edit_message": "编辑消息",
|
||||
"chat_panel_edited_a_file": "已编辑文件",
|
||||
@@ -470,6 +474,7 @@
|
||||
"tour_demo_project": "演示项目",
|
||||
"tour_look_through_the_agent_conversations_experiments_runs_and": "浏览智能体对话、实验、运行和产物,了解 OpenResearch 项目是如何形成的。",
|
||||
"tour_nanochat": "nanochat",
|
||||
"tour_run_demo_experiment": "试试演示实验",
|
||||
"tour_this_is_a_demo_project_showing_how_open": "这是一个展示 OpenResearch 工作方式的演示项目。本演示使用 Andrej Karpathy 的",
|
||||
"tour_welcome_to_open_research": "欢迎使用 OpenResearch",
|
||||
"tree_view_ask_in_this_task_to_create_one_or": "在此任务中请求创建实验,或切换到“整个项目”查看所有实验。",
|
||||
@@ -745,7 +750,6 @@
|
||||
"artifacts_too_large_to_preview": "文件太大,无法内嵌预览。",
|
||||
"artifacts_open_raw": "打开原始文件",
|
||||
"artifacts_copy_path": "复制路径",
|
||||
"tour_explore_demo": "探索演示项目",
|
||||
"experiment_latest_run": "最新运行",
|
||||
"experiment_runs": "运行",
|
||||
"experiments_none_yet": "还没有实验。",
|
||||
|
||||
+20
-27
@@ -28,6 +28,7 @@ import {
|
||||
type TaskWorkspace,
|
||||
} from "./workspaceState";
|
||||
import { getRememberedGlobalWorkspace, globalWorkspaceWriter } from "./workspacePersistence";
|
||||
import { PANEL_MIN_WIDTH, initialPanelWidth, panelMaxWidth } from "./panelLayout";
|
||||
import {
|
||||
type ExpViewDef,
|
||||
sameExpTab,
|
||||
@@ -82,7 +83,6 @@ import {
|
||||
cancelRun,
|
||||
DEMO_MAIN_SESSION_ID,
|
||||
DEMO_OVERVIEW_ARTIFACT,
|
||||
DEMO_RUN_EXPERIMENT_PROMPT,
|
||||
captureUiEvent,
|
||||
isDemoProjectId,
|
||||
type FirstAction,
|
||||
@@ -221,32 +221,14 @@ function fileBranchLabel(tab: FileViewDef, baselineBranch?: string): string | un
|
||||
|
||||
type ExperimentsView = "tree" | "table";
|
||||
|
||||
/** Floating panel sizing: keep both the panel and the chat column usable. */
|
||||
const PANEL_MIN_WIDTH = 360;
|
||||
const PANEL_MARGIN = 10;
|
||||
const WORKSPACE_CARD_MIN_WIDTH = 1448; // 1420px content plus the body’s 14px gutters.
|
||||
// Space the rest of the layout needs beside the panel: the 272px rail, the
|
||||
// chat column's minimum, and the gutters/margins between the three columns
|
||||
// (app-body padding 14×2, rail inner margin 14, end-pane inner margin 14).
|
||||
const RAIL_WIDTH = 272;
|
||||
const CHAT_MIN_SPACE = 380;
|
||||
const LAYOUT_CHROME = RAIL_WIDTH + 14 * 4;
|
||||
// Once a drag pushes the panel past its usable max by this much, it snaps to
|
||||
// fullscreen — a bit of resistance you have to overcome deliberately.
|
||||
const FULLSCREEN_SNAP_SLOP = 80;
|
||||
// Inward drag needed before snapping back to the last non-fullscreen width.
|
||||
const FULLSCREEN_RESTORE_DRAG = 48;
|
||||
|
||||
/** The widest the floating panel can be while leaving the rail + chat usable. */
|
||||
function panelMaxWidth(): number {
|
||||
return Math.max(PANEL_MIN_WIDTH, window.innerWidth - LAYOUT_CHROME - CHAT_MIN_SPACE);
|
||||
}
|
||||
|
||||
function initialPanelWidth(): number {
|
||||
const max = panelMaxWidth();
|
||||
return Math.max(PANEL_MIN_WIDTH, Math.min(760, max, Math.round(window.innerWidth * 0.4)));
|
||||
}
|
||||
|
||||
function upsert<T extends { id: string }>(list: T[], item: T): T[] {
|
||||
const i = list.findIndex((x) => x.id === item.id);
|
||||
if (i < 0) return [...list, item];
|
||||
@@ -651,7 +633,7 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
}, [setContentTabOrder, setPreviewTab]);
|
||||
const { ready: workspaceReady, loaded: workspaceLoaded, error: workspaceError, retry: retryWorkspace, capture: captureWorkspace, workspace: workspaceRef } = useProjectWorkspace({
|
||||
projectId: uiState && sessions !== null && (destination?.kind !== "task" || !activeSessionId || sessions.includes(activeSessionId)) ? projectId : null, taskKey: activeSessionId ?? "new", location: location.href, pane,
|
||||
isTask: destination?.kind === "task", demoOverview: uiState?.tourCompleted === false, state: rightPaneState,
|
||||
isTask: destination?.kind === "task", firstDemoOpen: uiState?.tourCompleted === false, state: rightPaneState,
|
||||
apply: applyWorkspace, getScroll: getFileScroll,
|
||||
sourceModes: sourceModesRef.current, revision: metadataRevision,
|
||||
});
|
||||
@@ -697,6 +679,8 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
}, [location.href, uiState, workspaceReady, railOpen, panelWidth, view]);
|
||||
const onboarded = uiState?.onboardingCompleted ?? false;
|
||||
const [demoWelcomeOpen, setDemoWelcomeOpen] = useState(false);
|
||||
const [demoRunningRunId, setDemoRunningRunId] = useState<string | null>(null);
|
||||
const [composerFocusNonce, setComposerFocusNonce] = useState(0);
|
||||
const openDemoWelcome = useCallback(() => setDemoWelcomeOpen(true), []);
|
||||
const closeDemoWelcome = useCallback(async (choice: "explore_demo" | "create_project" | "dismiss") => {
|
||||
const firstClose = tourCompletedRef.current === false;
|
||||
@@ -704,6 +688,7 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
if (firstClose) captureUiEvent({ name: "demo_welcome_choice", choice });
|
||||
setUiState((current) => current && { ...current, tourCompleted: saved.tourCompleted });
|
||||
setDemoWelcomeOpen(false);
|
||||
if (choice === "explore_demo") setComposerFocusNonce((n) => n + 1);
|
||||
}, []);
|
||||
const createProjectFromDemoWelcome = useCallback(async () => {
|
||||
await closeDemoWelcome("create_project");
|
||||
@@ -841,6 +826,13 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
}
|
||||
runsBaselineReadyRef.current = true;
|
||||
setRunDataReady(true);
|
||||
if (isDemoProjectId(baselineProjectId)) {
|
||||
setDemoRunningRunId((current) =>
|
||||
loadedRuns.some((run) => run.id === current && run.status === "running")
|
||||
? current
|
||||
: loadedRuns.find((run) => run.status === "running")?.id ?? null,
|
||||
);
|
||||
}
|
||||
if (shouldAutoOpen) openExperimentsTab(true);
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -887,6 +879,12 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
if (previous && previous.updatedAt > run.updatedAt) return;
|
||||
observedRunsRef.current.set(run.id, run);
|
||||
liveRunIdsRef.current.add(run.id);
|
||||
if (isDemoProjectId(run.projectId)) {
|
||||
setDemoRunningRunId((current) => {
|
||||
if (run.status === "running") return current ?? run.id;
|
||||
return current === run.id ? null : current;
|
||||
});
|
||||
}
|
||||
if (run.status !== "running" || previous?.status === "running") return;
|
||||
const baselineRun = baselineRunsRef.current.get(run.id);
|
||||
const newSinceBaseline =
|
||||
@@ -1586,13 +1584,8 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
|
||||
experimentName={experimentName}
|
||||
onOpenPlan={openPlanTab}
|
||||
onOpenSubagent={openSubagentTab}
|
||||
composerPrefill={
|
||||
activeProject &&
|
||||
isDemoProjectId(activeProject.id) &&
|
||||
uiState?.tourCompleted === false
|
||||
? DEMO_RUN_EXPERIMENT_PROMPT
|
||||
: null
|
||||
}
|
||||
composerFocusNonce={composerFocusNonce}
|
||||
demoRunningRunId={demoRunningRunId}
|
||||
runtime={runtime}
|
||||
onOpenDemoWelcome={
|
||||
activeProject && isDemoProjectId(activeProject.id) ? openDemoWelcome : undefined
|
||||
|
||||
@@ -24,6 +24,12 @@ export const DEMO_EXPERIMENT_LABELS: Record<string, string> = {
|
||||
[DEMO_LITERATURE_SESSION_ID]: "literature",
|
||||
};
|
||||
export const DEMO_OVERVIEW_ARTIFACT = "cpu-apple-silicon-pipeline-results.md";
|
||||
/** Leaf message each recorded demo session is seeded with; a send moves it. */
|
||||
export const DEMO_SEEDED_LEAF_IDS: Record<string, string> = {
|
||||
[DEMO_MAIN_SESSION_ID]: "msg_demo_nanochat_assistant_v1",
|
||||
[DEMO_FIGURE_SESSION_ID]: "msg_demo_nanochat_figures_assistant_v1",
|
||||
[DEMO_LITERATURE_SESSION_ID]: "msg_demo_nanochat_literature_assistant_v1",
|
||||
};
|
||||
export const DEMO_RUN_EXPERIMENT_PROMPT =
|
||||
"Run the Muon matrix LR 2× probe experiment. When it finishes, compare its step-100 and step-200 val_bpb against the baseline and tell me whether doubling the matrix learning rate helps early training.";
|
||||
|
||||
@@ -1835,6 +1841,7 @@ export interface ChatSession {
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
busy: boolean;
|
||||
activeLeafId: string | null;
|
||||
contextUsage?: ContextUsage;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
Search,
|
||||
SlidersHorizontal,
|
||||
SquareTerminal,
|
||||
Terminal,
|
||||
ToggleRight,
|
||||
TriangleAlert,
|
||||
Users,
|
||||
@@ -79,6 +80,8 @@ import {
|
||||
captureUiEvent,
|
||||
DEMO_EXPERIMENT_LABELS,
|
||||
DEMO_PROJECT_ID,
|
||||
DEMO_RUN_EXPERIMENT_PROMPT,
|
||||
DEMO_SEEDED_LEAF_IDS,
|
||||
forkChatTurn,
|
||||
fmtNumber,
|
||||
createRemoteSession,
|
||||
@@ -4222,7 +4225,8 @@ export function ChatPanel({
|
||||
onOpenSubagent,
|
||||
runtime,
|
||||
onOpenDemoWelcome,
|
||||
composerPrefill = null,
|
||||
composerFocusNonce = 0,
|
||||
demoRunningRunId = null,
|
||||
activeSessionId,
|
||||
onActiveSessionChange,
|
||||
preferredAgent,
|
||||
@@ -4278,7 +4282,10 @@ export function ChatPanel({
|
||||
runtime: RuntimeInfo;
|
||||
/** Reopen the demo welcome modal from the chat header. */
|
||||
onOpenDemoWelcome?: () => void;
|
||||
composerPrefill?: string | null;
|
||||
/** Increments when the demo welcome hands focus to the composer. */
|
||||
composerFocusNonce?: number;
|
||||
/** Demo run currently executing, for the monitor-it hint above the composer. */
|
||||
demoRunningRunId?: string | null;
|
||||
activeSessionId: string | null;
|
||||
onActiveSessionChange: (sessionId: string | null, options?: { replace?: boolean }) => void;
|
||||
/** Database-backed selection used to seed new chat sessions. */
|
||||
@@ -4319,6 +4326,8 @@ export function ChatPanel({
|
||||
const [unreadSessionIds, setUnreadSessionIds] = useState<ReadonlySet<string>>(new Set());
|
||||
const [sessionFilter, setSessionFilter] = useState<SessionFilter>("active");
|
||||
const [draft, setDraft] = useState("");
|
||||
const [demoHintDismissed, setDemoHintDismissed] = useState(false);
|
||||
const [demoRunHintDismissed, setDemoRunHintDismissed] = useState(false);
|
||||
const [annotations, setAnnotations] = useState<ComposerAnnotation[]>([]);
|
||||
const annotationId = useRef(0);
|
||||
const composerScopeRef = useRef({ projectId, activeId, mainView });
|
||||
@@ -4793,6 +4802,8 @@ export function ChatPanel({
|
||||
: new Set(),
|
||||
);
|
||||
setDraft("");
|
||||
setDemoHintDismissed(false);
|
||||
setDemoRunHintDismissed(false);
|
||||
setAttachments([]);
|
||||
setTitleReveals(new Map());
|
||||
seenTitles.current = new Map();
|
||||
@@ -5102,14 +5113,36 @@ export function ChatPanel({
|
||||
setComposerCursor(prompt.length);
|
||||
});
|
||||
};
|
||||
// Seeds the draft while a prefill is offered without taking focus, which may
|
||||
// belong to the demo welcome dialog; clearing the prefill later leaves the draft.
|
||||
// On offer until the user has sent anything in the demo: a send either adds
|
||||
// a session or moves a recorded session's leaf off its seeded message.
|
||||
const composerPrefill =
|
||||
projectId === DEMO_PROJECT_ID &&
|
||||
sessions.length > 0 &&
|
||||
sessions.every((session) => DEMO_SEEDED_LEAF_IDS[session.id] === session.activeLeafId)
|
||||
? DEMO_RUN_EXPERIMENT_PROMPT
|
||||
: null;
|
||||
// Seeds without taking focus; focus may still belong to the welcome dialog.
|
||||
useEffect(() => {
|
||||
if (!composerPrefill) return;
|
||||
setDraft(composerPrefill);
|
||||
setDraft((current) => current || composerPrefill);
|
||||
setSkillMenuDismissed(false);
|
||||
setComposerCursor(composerPrefill.length);
|
||||
}, [composerPrefill]);
|
||||
useEffect(() => {
|
||||
if (composerFocusNonce === 0) return;
|
||||
// The welcome dialog restores its previous focus on unmount; run after that.
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
const el = composerRef.current;
|
||||
if (!el) return;
|
||||
el.focus();
|
||||
el.setSelectionRange(el.value.length, el.value.length);
|
||||
el.scrollIntoView({ block: "nearest" });
|
||||
});
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [composerFocusNonce]);
|
||||
const demoHintId = useId();
|
||||
const demoHintVisible =
|
||||
projectId === DEMO_PROJECT_ID && draft === DEMO_RUN_EXPERIMENT_PROMPT && !demoHintDismissed;
|
||||
const updateTranscriptBottom = useCallback((el: HTMLDivElement) => {
|
||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 60;
|
||||
stickToBottom.current = atBottom;
|
||||
@@ -6213,6 +6246,59 @@ export function ChatPanel({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{demoHintVisible && (
|
||||
<div
|
||||
id={demoHintId}
|
||||
role="note"
|
||||
className={`composer-demo-hint ${COMPOSER_HINT_CLASS}`}
|
||||
>
|
||||
<FlaskConical size={16} className="shrink-0 text-primary" />
|
||||
<span className="flex-1" dir="auto">{m.chat_panel_demo_hint_body()}</span>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={m.chat_panel_dismiss_demo_hint()}
|
||||
title={m.chat_panel_dismiss_demo_hint()}
|
||||
onClick={() => {
|
||||
setDemoHintDismissed(true);
|
||||
composerRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
{demoRunningRunId && !demoRunHintDismissed && onOpenRun && (
|
||||
<div
|
||||
role="note"
|
||||
className={`composer-demo-run-hint ${COMPOSER_HINT_CLASS}`}
|
||||
>
|
||||
<FlaskConical size={16} className="shrink-0 text-primary" />
|
||||
<span className="flex flex-1 flex-wrap items-center gap-x-1.5 gap-y-1" dir="auto">
|
||||
<span>{m.chat_panel_demo_run_hint_before()}</span>
|
||||
<Button size="small" onClick={() => onOpenRun(demoRunningRunId, "keepOpen")}>
|
||||
<Terminal size={14} />
|
||||
{m.experiments_table_logs()}
|
||||
</Button>
|
||||
<span>{m.chat_panel_demo_run_hint_after()}</span>
|
||||
</span>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={m.chat_panel_dismiss_demo_hint()}
|
||||
title={m.chat_panel_dismiss_demo_hint()}
|
||||
onClick={() => {
|
||||
setDemoRunHintDismissed(true);
|
||||
composerRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
<span className="sr-only" role="status" aria-live="polite">
|
||||
{demoRunningRunId
|
||||
? `${m.chat_panel_demo_run_hint_before()} ${m.experiments_table_logs()} ${m.chat_panel_demo_run_hint_after()}`
|
||||
: ""}
|
||||
</span>
|
||||
<div className={`composer-box relative flex flex-col border ${bashActive ? "border-accent-amber" : "border-border"} rounded-lg bg-background shadow-elevated`} data-onboarding="composer">
|
||||
{activeHarness && !activeHarness.agentReady && (
|
||||
<div className="composer-harness-warning py-2 px-3 text-subtext text-sm leading-normal border-b border-b-border-variant [&_strong]:text-accent-amber [&_strong]:font-medium [&_code]:font-mono [&_code]:text-text">
|
||||
@@ -6282,6 +6368,7 @@ export function ChatPanel({
|
||||
<textarea
|
||||
dir="auto"
|
||||
ref={composerRef}
|
||||
aria-describedby={demoHintVisible ? demoHintId : undefined}
|
||||
// Native prose stays visible; the aligned mirror paints only skill tokens.
|
||||
className="relative z-1 bg-transparent"
|
||||
value={draft}
|
||||
@@ -6491,7 +6578,7 @@ export function ChatPanel({
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton
|
||||
className="send-btn"
|
||||
className={`send-btn ${demoHintVisible && activeHarness?.agentReady ? "ring-2 ring-primary ring-offset-2 ring-offset-background" : ""}`}
|
||||
variant="primary"
|
||||
title={bashActive ? m.chat_panel_run() : m.chat_panel_send()}
|
||||
aria-label={bashActive ? m.chat_panel_run() : m.chat_panel_send()}
|
||||
@@ -6514,6 +6601,8 @@ export function ChatPanel({
|
||||
);
|
||||
}
|
||||
|
||||
const COMPOSER_HINT_CLASS =
|
||||
"flex items-center gap-2.5 mb-2.5 py-2 ps-3.5 pe-2 rounded-lg border border-border bg-surface text-text text-sm leading-normal";
|
||||
const EMPTY_SKILLS: SkillInfo[] = [];
|
||||
|
||||
const EMPTY_HARNESSES: Harness[] = [];
|
||||
|
||||
@@ -138,7 +138,7 @@ export function DemoWelcomeModal({
|
||||
onClick={() => run(() => onClose("explore_demo"))}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? m.common_saving() : m.tour_explore_demo()}
|
||||
{saving ? m.common_saving() : m.tour_run_demo_experiment()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Floating panel sizing: keep both the panel and the chat column usable.
|
||||
export const PANEL_MIN_WIDTH = 360;
|
||||
const RAIL_WIDTH = 272;
|
||||
const CHAT_MIN_SPACE = 380;
|
||||
// Four 14px gutters: app-body padding ×2, rail inner margin, end-pane inner margin.
|
||||
const LAYOUT_CHROME = RAIL_WIDTH + 14 * 4;
|
||||
|
||||
/** The widest the floating panel can be while leaving the rail + chat usable. */
|
||||
export function panelMaxWidth(): number {
|
||||
return Math.max(PANEL_MIN_WIDTH, window.innerWidth - LAYOUT_CHROME - CHAT_MIN_SPACE);
|
||||
}
|
||||
|
||||
/** Default when no layout is saved. */
|
||||
export function initialPanelWidth(): number {
|
||||
const max = panelMaxWidth();
|
||||
return Math.max(PANEL_MIN_WIDTH, Math.min(480, max, Math.round(window.innerWidth * 0.25)));
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { useRuntime } from "./RemoteRuntime";
|
||||
import { clearReadDemoSessions } from "./demoSessionState";
|
||||
import { globalResumeLocation, projectResumeLocation } from "./routeResume";
|
||||
import { getRememberedGlobalWorkspace, globalWorkspaceWriter } from "./workspacePersistence";
|
||||
import { initialPanelWidth } from "./panelLayout";
|
||||
import { m } from "./paraglide/messages.js";
|
||||
import { Onboarding } from "./components/Onboarding";
|
||||
import { ProjectsHome } from "./components/ProjectsHome";
|
||||
@@ -79,7 +80,7 @@ export function ProjectsPage() {
|
||||
document.title = "OpenResearch";
|
||||
if (!state) return;
|
||||
globalWorkspaceWriter.queue({
|
||||
...(getRememberedGlobalWorkspace() ?? state.workspace ?? { railOpen: true, panelWidth: 760, experimentsView: "table" }),
|
||||
...(getRememberedGlobalWorkspace() ?? state.workspace ?? { railOpen: true, panelWidth: initialPanelWidth(), experimentsView: "table" }),
|
||||
lastLocation: "/projects",
|
||||
});
|
||||
}, [state]);
|
||||
|
||||
@@ -103,7 +103,7 @@ interface Props {
|
||||
location: string;
|
||||
pane: Pane | undefined;
|
||||
isTask: boolean;
|
||||
demoOverview: boolean;
|
||||
firstDemoOpen: boolean;
|
||||
state: RightPaneSessionState;
|
||||
apply: (state: RightPaneSessionState, saved: TaskWorkspace | undefined, restored: boolean) => void;
|
||||
getScroll: () => TaskWorkspace["scroll"];
|
||||
@@ -139,7 +139,7 @@ export function useProjectWorkspace(props: Props): {
|
||||
capture: () => void;
|
||||
workspace: MutableRefObject<ProjectWorkspace>;
|
||||
} {
|
||||
const { projectId, taskKey, location, pane, isTask, demoOverview, state, apply, getScroll, sourceModes, revision } = props;
|
||||
const { projectId, taskKey, location, pane, isTask, firstDemoOpen, state, apply, getScroll, sourceModes, revision } = props;
|
||||
const workspace = useRef<ProjectWorkspace>(emptyProjectWorkspace());
|
||||
const saveError = useSyncExternalStore(subscribeErrors, () => projectId ? saveErrors.get(projectId) ?? null : null);
|
||||
const [readError, setReadError] = useState<string | null>(null);
|
||||
@@ -193,7 +193,7 @@ export function useProjectWorkspace(props: Props): {
|
||||
appliedScope.current = scope;
|
||||
appliedPane.current = paneKey;
|
||||
if (isTask) {
|
||||
const saved = getTaskWorkspace(document, taskKey) ?? (isDemoProjectId(projectId) ? defaultTaskWorkspace(taskKey, demoOverview) : undefined);
|
||||
const saved = getTaskWorkspace(document, taskKey) ?? (isDemoProjectId(projectId) ? defaultTaskWorkspace(taskKey, firstDemoOpen) : undefined);
|
||||
apply(restoreWorkspace(saved, pane), saved, true);
|
||||
}
|
||||
setRenderedScope(scope);
|
||||
@@ -213,7 +213,7 @@ export function useProjectWorkspace(props: Props): {
|
||||
committed.current = { projectId, taskKey, scope, location, pane, state: resolvedState };
|
||||
} else save(projectId, location);
|
||||
workspace.current = projectCache.get(projectId) ?? document;
|
||||
}, [projectId, taskKey, location, pane, paneKey, isTask, demoOverview, state, apply, getScroll, sourceModes, revision, loadedProject, scope, renderedScope, capture]);
|
||||
}, [projectId, taskKey, location, pane, paneKey, isTask, firstDemoOpen, state, apply, getScroll, sourceModes, revision, loadedProject, scope, renderedScope, capture]);
|
||||
|
||||
useEffect(() => {
|
||||
const visit = epoch;
|
||||
|
||||
+8
-14
@@ -1,7 +1,7 @@
|
||||
import type { ExperimentView } from "./components/DetailDrawer";
|
||||
import type { CodeView } from "./components/CodeTab";
|
||||
import type { WorktreeView } from "./components/WorktreeTab";
|
||||
import { DEMO_MAIN_SESSION_ID, DEMO_FIGURE_SESSION_ID, DEMO_LITERATURE_SESSION_ID, DEMO_OVERVIEW_ARTIFACT } from "./api";
|
||||
import { DEMO_MAIN_SESSION_ID, DEMO_FIGURE_SESSION_ID, DEMO_LITERATURE_SESSION_ID } from "./api";
|
||||
import type { Pane, TaskWorkspace } from "./workspaceState";
|
||||
|
||||
export function tabPane(tab: RightTab, runId?: string | null): Pane {
|
||||
@@ -240,7 +240,7 @@ export interface RightPaneSessionState {
|
||||
|
||||
export function initialRightPaneSessionState(
|
||||
sessionId?: string,
|
||||
openDemoOverview = false,
|
||||
firstDemoOpen = false,
|
||||
): RightPaneSessionState {
|
||||
const initial: RightPaneSessionState = {
|
||||
rightTab: "experiments",
|
||||
@@ -263,21 +263,15 @@ export function initialRightPaneSessionState(
|
||||
panelMax: false,
|
||||
treeViewport: null,
|
||||
};
|
||||
if (sessionId === DEMO_MAIN_SESSION_ID && openDemoOverview) {
|
||||
const demoOverviewTab: FileViewDef = {
|
||||
path: DEMO_OVERVIEW_ARTIFACT,
|
||||
source: "artifacts",
|
||||
};
|
||||
// First demo open leads with the experiments tab so the idle follow-ups
|
||||
// are visible next to the prefilled prompt that runs one of them.
|
||||
if (sessionId === DEMO_MAIN_SESSION_ID && firstDemoOpen) {
|
||||
// First demo open shows only the experiments tab so the idle follow-ups
|
||||
// sit next to the prefilled prompt that runs one of them.
|
||||
const experimentsTab: RightTab = "experiments";
|
||||
return {
|
||||
...initial,
|
||||
rightTab: experimentsTab,
|
||||
tabHistory: [demoOverviewTab, experimentsTab],
|
||||
tabHistory: [experimentsTab],
|
||||
experimentsTabOpen: true,
|
||||
fileTabs: [demoOverviewTab],
|
||||
contentTabOrder: [rightTabKey(demoOverviewTab)],
|
||||
panelOpen: true,
|
||||
};
|
||||
}
|
||||
@@ -313,8 +307,8 @@ export function initialRightPaneSessionState(
|
||||
return initial;
|
||||
}
|
||||
|
||||
export function defaultTaskWorkspace(sessionId: string | undefined, openDemoOverview: boolean): TaskWorkspace | undefined {
|
||||
const state = initialRightPaneSessionState(sessionId, openDemoOverview);
|
||||
export function defaultTaskWorkspace(sessionId: string | undefined, firstDemoOpen: boolean): TaskWorkspace | undefined {
|
||||
const state = initialRightPaneSessionState(sessionId, firstDemoOpen);
|
||||
return state.panelOpen ? rememberWorkspace(state, {}, {}) : undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ function load(name, dependencies) {
|
||||
}, exports, { addEventListener() {}, removeEventListener() {} });
|
||||
return exports;
|
||||
}
|
||||
const demo = { DEMO_MAIN_SESSION_ID: "demo-main", DEMO_FIGURE_SESSION_ID: "demo-figures", DEMO_LITERATURE_SESSION_ID: "demo-literature", DEMO_OVERVIEW_ARTIFACT: "overview.md" };
|
||||
const demo = { DEMO_MAIN_SESSION_ID: "demo-main", DEMO_FIGURE_SESSION_ID: "demo-figures", DEMO_LITERATURE_SESSION_ID: "demo-literature" };
|
||||
const tabs = load("workspaceTabs", { "./api": demo });
|
||||
const clean = (value) => JSON.parse(JSON.stringify(value));
|
||||
const deferred = () => {
|
||||
@@ -60,7 +60,7 @@ function host(api) {
|
||||
const hook = load("useProjectWorkspace", { react, "./api": { ...api, isDemoProjectId: () => false }, "./workspaceState": workspace, "./workspaceTabs": tabs });
|
||||
const readiness = [];
|
||||
let state = tabs.initialRightPaneSessionState();
|
||||
let props = { projectId: "p", taskKey: "one", isTask: true, demoOverview: false, location: "/projects/p/tasks/one", pane: undefined, sourceModes: {}, revision: 0 };
|
||||
let props = { projectId: "p", taskKey: "one", isTask: true, firstDemoOpen: false, location: "/projects/p/tasks/one", pane: undefined, sourceModes: {}, revision: 0 };
|
||||
const scroll = {};
|
||||
const getScroll = () => scroll;
|
||||
const apply = (next, saved, restored) => {
|
||||
|
||||
Reference in New Issue
Block a user