Skip to content

Commit fce967a

Browse files
committed
Simplify quick start and onboarding
1 parent 2808c4d commit fce967a

7 files changed

Lines changed: 223 additions & 7 deletions

File tree

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,24 @@ Requirements:
7070
- Node.js and npm
7171
- Wails CLI is optional
7272

73-
Build the frontend and start local HTTP mode:
73+
Recommended local start:
74+
75+
```powershell
76+
powershell -ExecutionPolicy Bypass -File .\scripts\start.ps1
77+
```
78+
79+
The script installs frontend dependencies when needed, rebuilds the UI when
80+
source files change, starts AgentMeter, and opens:
81+
82+
```text
83+
http://127.0.0.1:34115
84+
```
85+
86+
On first launch, click **Index Now** in the app. AgentMeter defaults to
87+
`%USERPROFILE%\.codex`, so no source path setup is required unless your Codex
88+
sessions live somewhere else.
89+
90+
Build the frontend and start local HTTP mode manually:
7491

7592
```powershell
7693
cd frontend

frontend/src/App.vue

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { computed, onMounted, ref } from 'vue'
2+
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
33
import { useRoute, useRouter } from 'vue-router'
44
import { message } from 'ant-design-vue'
55
import {
@@ -11,6 +11,7 @@ import {
1111
ToolOutlined
1212
} from '@ant-design/icons-vue'
1313
import { api, type Settings } from './api'
14+
import { APP_DATA_CHANGED_EVENT, type AppDataChangeDetail } from './events'
1415
1516
const route = useRoute()
1617
const router = useRouter()
@@ -40,6 +41,14 @@ async function loadSettings() {
4041
settings.value = await api.getSettings()
4142
}
4243
44+
async function handleAppDataChanged(event: Event) {
45+
const detail = (event as CustomEvent<AppDataChangeDetail>).detail
46+
await loadSettings()
47+
if (detail?.reason === 'index') {
48+
refreshKey.value += 1
49+
}
50+
}
51+
4352
async function indexNow(rebuild = false) {
4453
indexing.value = true
4554
try {
@@ -58,7 +67,13 @@ function navigate(path: string) {
5867
router.push(path)
5968
}
6069
61-
onMounted(loadSettings)
70+
onMounted(() => {
71+
loadSettings()
72+
window.addEventListener(APP_DATA_CHANGED_EVENT, handleAppDataChanged)
73+
})
74+
onBeforeUnmount(() => {
75+
window.removeEventListener(APP_DATA_CHANGED_EVENT, handleAppDataChanged)
76+
})
6277
</script>
6378

6479
<template>

frontend/src/events.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export type AppDataChangeReason = 'settings' | 'index'
2+
3+
export interface AppDataChangeDetail {
4+
reason: AppDataChangeReason
5+
}
6+
7+
export const APP_DATA_CHANGED_EVENT = 'agentmeter:data-changed'
8+
9+
export function notifyAppDataChanged(reason: AppDataChangeReason) {
10+
window.dispatchEvent(new CustomEvent<AppDataChangeDetail>(APP_DATA_CHANGED_EVENT, { detail: { reason } }))
11+
}

frontend/src/styles.css

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,10 @@ body {
645645
border-radius: var(--am-radius);
646646
}
647647

648+
.empty-callout-main {
649+
min-width: 0;
650+
}
651+
648652
.empty-callout-title {
649653
color: var(--am-text);
650654
font-size: 13px;
@@ -659,6 +663,36 @@ body {
659663
line-height: 18px;
660664
}
661665

666+
.empty-callout-actions {
667+
display: flex;
668+
flex: 0 0 auto;
669+
flex-wrap: wrap;
670+
justify-content: flex-end;
671+
gap: 8px;
672+
}
673+
674+
.empty-source-line {
675+
display: flex;
676+
align-items: center;
677+
min-width: 0;
678+
max-width: 720px;
679+
gap: 8px;
680+
height: 30px;
681+
margin-top: 8px;
682+
padding: 0 9px;
683+
color: var(--am-text-soft);
684+
background: #ffffff;
685+
border: 1px solid #fde68a;
686+
border-radius: var(--am-radius-sm);
687+
}
688+
689+
.empty-source-path {
690+
min-width: 0;
691+
color: var(--am-text-soft);
692+
font-size: 12px;
693+
font-variant-numeric: tabular-nums;
694+
}
695+
662696
.toolbar {
663697
display: flex;
664698
align-items: center;
@@ -1389,12 +1423,22 @@ body {
13891423
}
13901424

13911425
.content-grid,
1426+
.empty-callout,
13921427
.split-row,
13931428
.summary-panel,
13941429
.session-summary {
13951430
grid-template-columns: 1fr;
13961431
}
13971432

1433+
.empty-callout {
1434+
align-items: flex-start;
1435+
flex-direction: column;
1436+
}
1437+
1438+
.empty-callout-actions {
1439+
justify-content: flex-start;
1440+
}
1441+
13981442
.info-block-grid,
13991443
.index-result-grid {
14001444
grid-template-columns: repeat(2, minmax(0, 1fr));

frontend/src/views/Overview.vue

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
33
import { useRouter } from 'vue-router'
44
import * as echarts from 'echarts'
5+
import { message } from 'ant-design-vue'
56
import {
67
BarChartOutlined,
78
ClockCircleOutlined,
89
DollarCircleOutlined,
10+
FolderOpenOutlined,
911
FunctionOutlined,
12+
PlayCircleOutlined,
13+
SettingOutlined,
1014
TableOutlined,
1115
ToolOutlined
1216
} from '@ant-design/icons-vue'
@@ -19,17 +23,22 @@ import {
1923
shortPath,
2024
type ModelUsage,
2125
type Overview,
26+
type Settings,
2227
type Session
2328
} from '../api'
2429
import { chartPalette, usageChartColors } from '../chartPalette'
30+
import { notifyAppDataChanged } from '../events'
2531
2632
const router = useRouter()
2733
const loading = ref(true)
34+
const startupIndexing = ref(false)
2835
const overview = ref<Overview | null>(null)
36+
const settings = ref<Settings | null>(null)
2937
const chartEl = ref<HTMLDivElement | null>(null)
3038
let chart: echarts.ECharts | null = null
3139
3240
const hasIndexedData = computed(() => (overview.value?.totalSessions || 0) > 0)
41+
const sourcePathDisplay = computed(() => settings.value?.sourcePath || settings.value?.defaultSourcePath || '')
3342
const hasDailyUsage = computed(() => (overview.value?.dailyUsage?.length || 0) > 0)
3443
const rankedModelUsage = computed(() =>
3544
[...(overview.value?.modelUsage || [])].sort((left, right) => right.totalTokens - left.totalTokens)
@@ -56,14 +65,30 @@ const recentColumns = [
5665
async function load() {
5766
loading.value = true
5867
try {
59-
overview.value = await api.getOverview()
68+
const [settingsValue, overviewValue] = await Promise.all([api.getSettings(), api.getOverview()])
69+
settings.value = settingsValue
70+
overview.value = overviewValue
6071
} finally {
6172
loading.value = false
6273
}
6374
await nextTick()
6475
renderChart()
6576
}
6677
78+
async function indexFromOverview() {
79+
startupIndexing.value = true
80+
try {
81+
const result = await api.indexNow(false)
82+
message.success(`${result.indexed} indexed, ${result.skipped} skipped, ${result.failed} failed`)
83+
await load()
84+
notifyAppDataChanged('index')
85+
} catch (error) {
86+
message.error(error instanceof Error ? error.message : 'Index failed')
87+
} finally {
88+
startupIndexing.value = false
89+
}
90+
}
91+
6792
function renderChart() {
6893
const dailyUsage = overview.value?.dailyUsage || []
6994
if (!dailyUsage.length) {
@@ -230,13 +255,33 @@ onBeforeUnmount(() => {
230255
</section>
231256

232257
<div v-if="!loading && !hasIndexedData" class="empty-callout overview-empty-callout">
233-
<div>
258+
<div class="empty-callout-main">
234259
<div class="empty-callout-title">No indexed sessions yet</div>
235260
<div class="empty-callout-text">
236-
Configure a local source path, then run Index Now to populate usage, cost, and tool-call telemetry.
261+
AgentMeter can scan the configured Codex source now and refresh this dashboard when indexing completes.
237262
</div>
263+
<div class="empty-source-line">
264+
<FolderOpenOutlined />
265+
<span class="source-label">Source</span>
266+
<a-typography-text class="empty-source-path" :ellipsis="{ tooltip: sourcePathDisplay }">
267+
{{ sourcePathDisplay || 'Open Settings to choose a source path' }}
268+
</a-typography-text>
269+
</div>
270+
</div>
271+
<div class="empty-callout-actions">
272+
<a-button type="primary" :loading="startupIndexing" :disabled="!sourcePathDisplay" @click="indexFromOverview">
273+
<template #icon>
274+
<PlayCircleOutlined />
275+
</template>
276+
Index Now
277+
</a-button>
278+
<a-button @click="$router.push('/settings')">
279+
<template #icon>
280+
<SettingOutlined />
281+
</template>
282+
Edit Source
283+
</a-button>
238284
</div>
239-
<a-button type="primary" @click="$router.push('/settings')">Open Settings</a-button>
240285
</div>
241286

242287
<div class="content-grid overview-primary-grid">

frontend/src/views/Settings.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue'
33
import { message } from 'ant-design-vue'
44
import { DatabaseOutlined, FolderOpenOutlined, ReloadOutlined } from '@ant-design/icons-vue'
55
import { api, formatDateTime, formatDuration, formatNumber, type Settings } from '../api'
6+
import { notifyAppDataChanged } from '../events'
67
78
const loading = ref(true)
89
const saving = ref(false)
@@ -61,6 +62,7 @@ async function save() {
6162
saving.value = true
6263
try {
6364
settings.value = await api.saveSettings(sourcePath.value)
65+
notifyAppDataChanged('settings')
6466
message.success('Settings saved')
6567
} catch (error) {
6668
message.error(error instanceof Error ? error.message : 'Save failed')
@@ -75,6 +77,7 @@ async function index(rebuild = false) {
7577
const result = await api.indexNow(rebuild)
7678
message.success(`${result.indexed} indexed, ${result.skipped} skipped, ${result.failed} failed`)
7779
await load()
80+
notifyAppDataChanged('index')
7881
} catch (error) {
7982
message.error(error instanceof Error ? error.message : 'Index failed')
8083
} finally {

scripts/start.ps1

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
param(
2+
[int]$Port = 34115,
3+
[switch]$SkipBrowser,
4+
[switch]$ForceBuild
5+
)
6+
7+
$ErrorActionPreference = "Stop"
8+
9+
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
10+
$frontendDir = Join-Path $repoRoot "frontend"
11+
$distIndex = Join-Path $frontendDir "dist\index.html"
12+
$nodeModules = Join-Path $frontendDir "node_modules"
13+
$nodeModulesLock = Join-Path $nodeModules ".package-lock.json"
14+
$packageLock = Join-Path $frontendDir "package-lock.json"
15+
16+
function Test-Command($Name) {
17+
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
18+
}
19+
20+
function Get-NewestWriteTime($Path) {
21+
if (-not (Test-Path $Path)) {
22+
return [datetime]::MinValue
23+
}
24+
$items = Get-ChildItem -Path $Path -Recurse -File
25+
if (-not $items) {
26+
return [datetime]::MinValue
27+
}
28+
return ($items | Sort-Object LastWriteTime -Descending | Select-Object -First 1).LastWriteTime
29+
}
30+
31+
if (-not (Test-Command "go")) {
32+
throw "Go was not found on PATH."
33+
}
34+
if (-not (Test-Command "npm")) {
35+
throw "npm was not found on PATH."
36+
}
37+
38+
Push-Location $repoRoot
39+
try {
40+
if ((-not (Test-Path $nodeModules)) -or (Test-Path $packageLock -and (-not (Test-Path $nodeModulesLock) -or (Get-Item $packageLock).LastWriteTime -gt (Get-Item $nodeModulesLock).LastWriteTime))) {
41+
Write-Host "Installing frontend dependencies..."
42+
Push-Location $frontendDir
43+
try {
44+
npm ci
45+
} finally {
46+
Pop-Location
47+
}
48+
}
49+
50+
$needsBuild = $ForceBuild -or -not (Test-Path $distIndex)
51+
if (-not $needsBuild) {
52+
$distTime = (Get-Item $distIndex).LastWriteTime
53+
$srcTime = Get-NewestWriteTime (Join-Path $frontendDir "src")
54+
$publicTime = Get-NewestWriteTime (Join-Path $frontendDir "public")
55+
$packageTime = (Get-Item $packageLock).LastWriteTime
56+
$needsBuild = $srcTime -gt $distTime -or $publicTime -gt $distTime -or $packageTime -gt $distTime
57+
}
58+
59+
if ($needsBuild) {
60+
Write-Host "Building frontend..."
61+
Push-Location $frontendDir
62+
try {
63+
npm run build
64+
} finally {
65+
Pop-Location
66+
}
67+
}
68+
69+
$url = "http://127.0.0.1:$Port"
70+
Write-Host "Starting AgentMeter at $url"
71+
if (-not $SkipBrowser) {
72+
Start-Job -ScriptBlock {
73+
param($Target)
74+
Start-Sleep -Seconds 2
75+
Start-Process $Target
76+
} -ArgumentList $url | Out-Null
77+
}
78+
go run . -http ":$Port"
79+
} finally {
80+
Pop-Location
81+
}

0 commit comments

Comments
 (0)