Skip to content

Commit eb3ba64

Browse files
authored
cleaned up build output w/ "debug" lines and filtering (#2545)
1 parent 3a08c55 commit eb3ba64

File tree

14 files changed

+296
-151
lines changed

14 files changed

+296
-151
lines changed

frontend/app/store/wshclientapi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ class RpcApiType {
363363
}
364364

365365
// command "listalleditableapps" [call]
366-
ListAllEditableAppsCommand(client: WshClient, opts?: RpcOpts): Promise<string[]> {
366+
ListAllEditableAppsCommand(client: WshClient, opts?: RpcOpts): Promise<AppInfo[]> {
367367
return client.wshRpcCall("listalleditableapps", null, opts);
368368
}
369369

frontend/builder/app-selection-modal.tsx

Lines changed: 97 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,16 @@ import { RpcApi } from "@/app/store/wshclientapi";
66
import { TabRpcClient } from "@/app/store/wshrpcutil";
77
import { atoms, globalStore } from "@/store/global";
88
import * as WOS from "@/store/wos";
9+
import { formatRelativeTime } from "@/util/util";
910
import { useEffect, useState } from "react";
1011

1112
const MaxAppNameLength = 50;
1213
const AppNameRegex = /^[a-zA-Z0-9_-]+$/;
1314

14-
export function AppSelectionModal() {
15-
const [apps, setApps] = useState<string[]>([]);
16-
const [loading, setLoading] = useState(true);
15+
function CreateNewWaveApp({ onCreateApp }: { onCreateApp: (appName: string) => Promise<void> }) {
1716
const [newAppName, setNewAppName] = useState("");
18-
const [error, setError] = useState("");
1917
const [inputError, setInputError] = useState("");
20-
21-
useEffect(() => {
22-
loadApps();
23-
}, []);
18+
const [isCreating, setIsCreating] = useState(false);
2419

2520
const validateAppName = (name: string) => {
2621
if (!name.trim()) {
@@ -39,10 +34,83 @@ export function AppSelectionModal() {
3934
return true;
4035
};
4136

37+
const handleCreate = async () => {
38+
const trimmedName = newAppName.trim();
39+
if (!validateAppName(trimmedName)) {
40+
return;
41+
}
42+
43+
setIsCreating(true);
44+
try {
45+
await onCreateApp(trimmedName);
46+
} finally {
47+
setIsCreating(false);
48+
}
49+
};
50+
51+
return (
52+
<div className="min-h-[80px]">
53+
<h3 className="text-base font-medium mb-1 text-muted-foreground">Create New WaveApp</h3>
54+
<div className="relative">
55+
<div className="flex w-full">
56+
<input
57+
type="text"
58+
value={newAppName}
59+
onChange={(e) => {
60+
const value = e.target.value;
61+
setNewAppName(value);
62+
validateAppName(value);
63+
}}
64+
onKeyDown={(e) => {
65+
if (e.key === "Enter" && !e.nativeEvent.isComposing && newAppName.trim() && !inputError) {
66+
handleCreate();
67+
}
68+
}}
69+
placeholder="my-app"
70+
maxLength={MaxAppNameLength}
71+
className={`flex-1 px-3 py-2 bg-panel border rounded-l focus:outline-none transition-colors ${
72+
inputError ? "border-error" : "border-border focus:border-accent"
73+
}`}
74+
autoFocus
75+
disabled={isCreating}
76+
/>
77+
<button
78+
onClick={handleCreate}
79+
disabled={!newAppName.trim() || !!inputError || isCreating}
80+
className={`px-4 py-2 rounded-r transition-colors font-medium whitespace-nowrap ${
81+
!newAppName.trim() || inputError || isCreating
82+
? "bg-panel border border-l-0 border-border text-muted cursor-not-allowed"
83+
: "bg-accent text-black hover:bg-accent-hover cursor-pointer"
84+
}`}
85+
>
86+
Create
87+
</button>
88+
</div>
89+
{inputError && (
90+
<div className="absolute left-0 top-full mt-1 text-xs text-error flex items-center gap-1.5 whitespace-nowrap">
91+
<i className="fa-solid fa-circle-exclamation"></i>
92+
<span>{inputError}</span>
93+
</div>
94+
)}
95+
</div>
96+
</div>
97+
);
98+
}
99+
100+
export function AppSelectionModal() {
101+
const [apps, setApps] = useState<AppInfo[]>([]);
102+
const [loading, setLoading] = useState(true);
103+
const [error, setError] = useState("");
104+
105+
useEffect(() => {
106+
loadApps();
107+
}, []);
108+
42109
const loadApps = async () => {
43110
try {
44111
const appList = await RpcApi.ListAllEditableAppsCommand(TabRpcClient);
45-
setApps(appList || []);
112+
const sortedApps = (appList || []).sort((a, b) => b.modtime - a.modtime);
113+
setApps(sortedApps);
46114
} catch (err) {
47115
console.error("Failed to load apps:", err);
48116
setError("Failed to load apps");
@@ -61,25 +129,8 @@ export function AppSelectionModal() {
61129
globalStore.set(atoms.builderAppId, appId);
62130
};
63131

64-
const handleCreateNew = async () => {
65-
const trimmedName = newAppName.trim();
66-
67-
if (!trimmedName) {
68-
setError("WaveApp name cannot be empty");
69-
return;
70-
}
71-
72-
if (trimmedName.length > MaxAppNameLength) {
73-
setError(`WaveApp name must be ${MaxAppNameLength} characters or less`);
74-
return;
75-
}
76-
77-
if (!AppNameRegex.test(trimmedName)) {
78-
setError("WaveApp name can only contain letters, numbers, hyphens, and underscores");
79-
return;
80-
}
81-
82-
const draftAppId = `draft/${trimmedName}`;
132+
const handleCreateNew = async (appName: string) => {
133+
const draftAppId = `draft/${appName}`;
83134
const builderId = globalStore.get(atoms.builderId);
84135
const oref = WOS.makeORef("builder", builderId);
85136
await RpcApi.SetRTInfoCommand(TabRpcClient, {
@@ -111,9 +162,9 @@ export function AppSelectionModal() {
111162
}
112163

113164
return (
114-
<FlexiModal className="min-w-[600px] w-[600px] max-h-[80vh] overflow-y-auto">
165+
<FlexiModal className="min-w-[600px] w-[600px] max-h-[90vh] overflow-y-auto">
115166
<div className="w-full px-2 pt-0 pb-4">
116-
<h2 className="text-2xl mb-6">Select a WaveApp to Edit</h2>
167+
<h2 className="text-2xl mb-2">Select a WaveApp to Edit</h2>
117168

118169
{error && (
119170
<div className="mb-6 px-4 py-3 bg-panel rounded">
@@ -125,18 +176,23 @@ export function AppSelectionModal() {
125176
)}
126177

127178
{apps.length > 0 && (
128-
<div className="mb-6">
129-
<h3 className="text-base font-medium mb-3 text-muted-foreground">Existing WaveApps</h3>
130-
<div className="space-y-2 max-h-[200px] overflow-y-auto">
131-
{apps.map((appId) => (
179+
<div className="mb-2">
180+
<h3 className="text-base font-medium mb-1 text-muted-foreground">Existing WaveApps</h3>
181+
<div className="space-y-2 max-h-[220px] overflow-y-auto">
182+
{apps.map((appInfo) => (
132183
<button
133-
key={appId}
134-
onClick={() => handleSelectApp(appId)}
135-
className="w-full text-left px-4 py-3 bg-panel hover:bg-hover border border-border rounded transition-colors cursor-pointer"
184+
key={appInfo.appid}
185+
onClick={() => handleSelectApp(appInfo.appid)}
186+
className="w-full text-left px-4 py-1.5 bg-panel hover:bg-hover border border-border rounded transition-colors cursor-pointer"
136187
>
137188
<div className="flex items-center gap-3">
138-
<i className="fa-solid fa-cube"></i>
139-
<span>{getAppDisplayName(appId)}</span>
189+
<i className="fa-solid fa-cube self-center"></i>
190+
<div className="flex flex-col">
191+
<span>{getAppDisplayName(appInfo.appid)}</span>
192+
<span className="text-[11px] text-muted mt-0.5">
193+
Last updated: {formatRelativeTime(appInfo.modtime)}
194+
</span>
195+
</div>
140196
</div>
141197
</button>
142198
))}
@@ -145,62 +201,14 @@ export function AppSelectionModal() {
145201
)}
146202

147203
{apps.length > 0 && (
148-
<div className="flex items-center gap-4 my-6">
204+
<div className="flex items-center gap-4 my-2">
149205
<div className="flex-1 border-t border-border"></div>
150206
<span className="text-muted-foreground text-sm">or</span>
151207
<div className="flex-1 border-t border-border"></div>
152208
</div>
153209
)}
154210

155-
<div className="min-h-[80px]">
156-
<h3 className="text-base font-medium mb-4 text-muted-foreground">Create New WaveApp</h3>
157-
<div className="relative">
158-
<div className="flex w-full">
159-
<input
160-
type="text"
161-
value={newAppName}
162-
onChange={(e) => {
163-
const value = e.target.value;
164-
setNewAppName(value);
165-
validateAppName(value);
166-
}}
167-
onKeyDown={(e) => {
168-
if (
169-
e.key === "Enter" &&
170-
!e.nativeEvent.isComposing &&
171-
newAppName.trim() &&
172-
!inputError
173-
) {
174-
handleCreateNew();
175-
}
176-
}}
177-
placeholder="my-app"
178-
maxLength={MaxAppNameLength}
179-
className={`flex-1 px-3 py-2 bg-panel border rounded-l focus:outline-none transition-colors ${
180-
inputError ? "border-error" : "border-border focus:border-accent"
181-
}`}
182-
autoFocus
183-
/>
184-
<button
185-
onClick={handleCreateNew}
186-
disabled={!newAppName.trim() || !!inputError}
187-
className={`px-4 py-2 rounded-r transition-colors font-medium whitespace-nowrap ${
188-
!newAppName.trim() || inputError
189-
? "bg-panel border border-l-0 border-border text-muted cursor-not-allowed"
190-
: "bg-accent text-black hover:bg-accent-hover cursor-pointer"
191-
}`}
192-
>
193-
Create
194-
</button>
195-
</div>
196-
{inputError && (
197-
<div className="absolute left-0 top-full mt-1 text-xs text-error flex items-center gap-1.5 whitespace-nowrap">
198-
<i className="fa-solid fa-circle-exclamation"></i>
199-
<span>{inputError}</span>
200-
</div>
201-
)}
202-
</div>
203-
</div>
211+
<CreateNewWaveApp onCreateApp={handleCreateNew} />
204212
</div>
205213
</FlexiModal>
206214
);

frontend/builder/builder-buildpanel.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import { WaveAIModel } from "@/app/aipanel/waveai-model";
55
import { ContextMenuModel } from "@/app/store/contextmenu";
6+
import { globalStore } from "@/app/store/jotaiStore";
67
import { BuilderBuildPanelModel } from "@/builder/store/builder-buildpanel-model";
78
import { useAtomValue } from "jotai";
89
import { memo, useCallback, useEffect, useRef } from "react";
@@ -35,6 +36,7 @@ function handleBuildPanelContextMenu(e: React.MouseEvent, selectedText: string):
3536
const BuilderBuildPanel = memo(() => {
3637
const model = BuilderBuildPanelModel.getInstance();
3738
const outputLines = useAtomValue(model.outputLines);
39+
const showDebug = useAtomValue(model.showDebug);
3840
const scrollRef = useRef<HTMLDivElement>(null);
3941
const preRef = useRef<HTMLPreElement>(null);
4042

@@ -71,10 +73,25 @@ const BuilderBuildPanel = memo(() => {
7173
handleBuildPanelContextMenu(e, selectedText);
7274
}, []);
7375

76+
const handleDebugToggle = useCallback(() => {
77+
globalStore.set(model.showDebug, !showDebug);
78+
}, [model, showDebug]);
79+
80+
const filteredLines = showDebug ? outputLines : outputLines.filter((line) => !line.startsWith("[debug]"));
81+
7482
return (
7583
<div className="w-full h-full flex flex-col bg-black">
76-
<div className="flex-shrink-0 px-3 py-2 border-b border-gray-700">
84+
<div className="flex-shrink-0 px-3 py-2 border-b border-gray-700 flex items-center justify-between">
7785
<span className="text-sm font-semibold text-gray-300">Build Output</span>
86+
<label className="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
87+
<input
88+
type="checkbox"
89+
checked={showDebug}
90+
onChange={handleDebugToggle}
91+
className="cursor-pointer"
92+
/>
93+
Debug
94+
</label>
7895
</div>
7996
<div ref={scrollRef} className="flex-1 overflow-y-auto overflow-x-auto p-2">
8097
<pre
@@ -83,10 +100,11 @@ const BuilderBuildPanel = memo(() => {
83100
onMouseUp={handleMouseUp}
84101
onContextMenu={handleContextMenu}
85102
>
86-
{outputLines.length === 0 ? (
103+
{/* this comment fixes JSX blank line in pre tag */}
104+
{filteredLines.length === 0 ? (
87105
<span className="text-secondary">Waiting for output...</span>
88106
) : (
89-
outputLines.join("\n")
107+
filteredLines.join("\n")
90108
)}
91109
</pre>
92110
</div>

frontend/builder/store/builder-buildpanel-model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class BuilderBuildPanelModel {
1212
private static instance: BuilderBuildPanelModel | null = null;
1313

1414
outputLines: PrimitiveAtom<string[]> = atom<string[]>([]);
15+
showDebug: PrimitiveAtom<boolean> = atom<boolean>(false);
1516
outputUnsubFn: (() => void) | null = null;
1617
initialized = false;
1718

frontend/types/gotypes.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ declare global {
5656
message?: string;
5757
};
5858

59+
// wshrpc.AppInfo
60+
type AppInfo = {
61+
appid: string;
62+
modtime: number;
63+
};
64+
5965
// waveobj.Block
6066
type Block = WaveObj & {
6167
parentoref?: string;

frontend/util/util.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,29 @@ function parseDataUrl(dataUrl: string): ParsedDataUrl {
452452
return { mimeType, buffer };
453453
}
454454

455+
function formatRelativeTime(timestamp: number): string {
456+
if (!timestamp) {
457+
return "never";
458+
}
459+
const now = Date.now();
460+
const diffInSeconds = Math.floor((now - timestamp) / 1000);
461+
const diffInMinutes = Math.floor(diffInSeconds / 60);
462+
const diffInHours = Math.floor(diffInMinutes / 60);
463+
const diffInDays = Math.floor(diffInHours / 24);
464+
465+
if (diffInMinutes <= 0) {
466+
return "Just now";
467+
} else if (diffInMinutes < 60) {
468+
return `${diffInMinutes} min${diffInMinutes !== 1 ? "s" : ""} ago`;
469+
} else if (diffInHours < 24) {
470+
return `${diffInHours} hr${diffInHours !== 1 ? "s" : ""} ago`;
471+
} else if (diffInDays < 7) {
472+
return `${diffInDays} day${diffInDays !== 1 ? "s" : ""} ago`;
473+
} else {
474+
return new Date(timestamp).toLocaleDateString();
475+
}
476+
}
477+
455478
export {
456479
atomWithDebounce,
457480
atomWithThrottle,
@@ -464,6 +487,7 @@ export {
464487
deepCompareReturnPrev,
465488
escapeBytes,
466489
fireAndForget,
490+
formatRelativeTime,
467491
getPrefixedSettings,
468492
getPromiseState,
469493
getPromiseValue,

0 commit comments

Comments
 (0)