feat: add devices page with goal input and step log
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
5
web/.env.example
Normal file
5
web/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# Replace with your DB credentials!
|
||||
DATABASE_URL="postgres://user:password@host:port/db-name"
|
||||
|
||||
SERVER_URL="http://localhost:8080"
|
||||
PUBLIC_SERVER_WS_URL="ws://localhost:8080"
|
||||
17
web/src/lib/api/devices.remote.ts
Normal file
17
web/src/lib/api/devices.remote.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { query, getRequestEvent } from '$app/server';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
const SERVER_URL = env.SERVER_URL || 'http://localhost:8080';
|
||||
|
||||
export const listDevices = query(async () => {
|
||||
const { request } = getRequestEvent();
|
||||
|
||||
const res = await fetch(`${SERVER_URL}/devices`, {
|
||||
headers: {
|
||||
cookie: request.headers.get('cookie') ?? ''
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) return [];
|
||||
return res.json();
|
||||
});
|
||||
36
web/src/routes/dashboard/devices/+page.svelte
Normal file
36
web/src/routes/dashboard/devices/+page.svelte
Normal file
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { listDevices } from '$lib/api/devices.remote';
|
||||
|
||||
const devices = await listDevices();
|
||||
</script>
|
||||
|
||||
<h2 class="mb-6 text-2xl font-bold">Devices</h2>
|
||||
|
||||
{#if devices.length === 0}
|
||||
<div class="rounded-lg border border-neutral-200 p-8 text-center">
|
||||
<p class="text-neutral-500">No devices connected.</p>
|
||||
<p class="mt-2 text-sm text-neutral-400">
|
||||
Install the Android app, paste your API key, and your device will appear here.
|
||||
</p>
|
||||
<a href="/dashboard/api-keys" class="mt-4 inline-block text-sm text-blue-600 hover:underline">
|
||||
Create an API key
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each devices as device (device.deviceId)}
|
||||
<a
|
||||
href="/dashboard/devices/{device.deviceId}"
|
||||
class="flex items-center justify-between rounded-lg border border-neutral-200 p-4 hover:border-neutral-400"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium">{device.name}</p>
|
||||
<p class="text-sm text-neutral-500">
|
||||
Connected {new Date(device.connectedAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-green-500"></span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
72
web/src/routes/dashboard/devices/[deviceId]/+page.svelte
Normal file
72
web/src/routes/dashboard/devices/[deviceId]/+page.svelte
Normal file
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
|
||||
const deviceId = page.params.deviceId;
|
||||
|
||||
let goal = $state('');
|
||||
let steps = $state<Array<{ step: number; action: string; reasoning: string }>>([]);
|
||||
let status = $state<'idle' | 'running' | 'completed' | 'failed'>('idle');
|
||||
|
||||
async function submitGoal() {
|
||||
if (!goal.trim()) return;
|
||||
status = 'running';
|
||||
steps = [];
|
||||
|
||||
const res = await fetch('/api/goals', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ deviceId, goal })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
status = 'failed';
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<h2 class="mb-6 text-2xl font-bold">Device: {deviceId.slice(0, 8)}...</h2>
|
||||
|
||||
<div class="max-w-2xl">
|
||||
<div class="mb-8 rounded-lg border border-neutral-200 p-6">
|
||||
<h3 class="mb-3 font-semibold">Send a Goal</h3>
|
||||
<div class="flex gap-3">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={goal}
|
||||
placeholder="e.g., Open YouTube and search for lofi beats"
|
||||
class="flex-1 rounded border border-neutral-300 px-3 py-2 text-sm focus:border-neutral-500 focus:outline-none"
|
||||
disabled={status === 'running'}
|
||||
/>
|
||||
<button
|
||||
onclick={submitGoal}
|
||||
disabled={status === 'running'}
|
||||
class="rounded bg-neutral-800 px-4 py-2 text-sm text-white hover:bg-neutral-700 disabled:opacity-50"
|
||||
>
|
||||
{status === 'running' ? 'Running...' : 'Run'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if steps.length > 0}
|
||||
<div class="rounded-lg border border-neutral-200">
|
||||
<div class="border-b border-neutral-200 px-6 py-4">
|
||||
<h3 class="font-semibold">Steps</h3>
|
||||
</div>
|
||||
<div class="divide-y divide-neutral-100">
|
||||
{#each steps as step (step.step)}
|
||||
<div class="px-6 py-3">
|
||||
<p class="text-sm font-medium">Step {step.step}: {step.action}</p>
|
||||
<p class="text-sm text-neutral-500">{step.reasoning}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if status === 'completed'}
|
||||
<p class="mt-4 text-sm text-green-600">Goal completed successfully.</p>
|
||||
{:else if status === 'failed'}
|
||||
<p class="mt-4 text-sm text-red-600">Goal failed. Please try again.</p>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user