Re org kohakuboard ui routes

This commit is contained in:
Kohaku-Blueleaf
2025-10-27 03:44:13 +08:00
parent a06bddb481
commit 3e9fbc36c7
7 changed files with 1501 additions and 64 deletions

View File

@@ -2,11 +2,13 @@
import { ref, onMounted } from "vue";
import { initializeSliderSync } from "@/composables/useSliderSync";
import { useAnimationPreference } from "@/composables/useAnimationPreference";
import { getSystemInfo } from "@/utils/api";
const darkMode = ref(false);
const { animationsEnabled, toggleAnimations } = useAnimationPreference();
const systemInfo = ref(null);
onMounted(() => {
onMounted(async () => {
const savedTheme = localStorage.getItem("theme");
darkMode.value =
savedTheme === "dark" ||
@@ -15,6 +17,13 @@ onMounted(() => {
// Initialize global slider synchronization
initializeSliderSync();
// Fetch system info for mode detection
try {
systemInfo.value = await getSystemInfo();
} catch (error) {
console.error("Failed to fetch system info:", error);
}
});
function toggleDarkMode() {
@@ -59,13 +68,13 @@ function updateTheme() {
to="/"
class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400"
>
Dashboard
Projects
</router-link>
<router-link
to="/experiments"
class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-blue-600 dark:hover:text-blue-400"
>
Experiments
Experiments (Legacy)
</router-link>
</div>
</div>

View File

@@ -1,24 +1,25 @@
<script setup>
import { ref, onMounted } from "vue";
import { fetchExperiments } from "@/utils/api";
import { fetchProjects } from "@/utils/api";
import { useRouter } from "vue-router";
const router = useRouter();
const experiments = ref([]);
const projects = ref([]);
const loading = ref(true);
onMounted(async () => {
try {
experiments.value = await fetchExperiments();
const result = await fetchProjects();
projects.value = result.projects;
} catch (error) {
console.error("Failed to fetch experiments:", error);
console.error("Failed to fetch projects:", error);
} finally {
loading.value = false;
}
});
function viewExperiment(id) {
router.push(`/experiments/${id}`);
function viewProject(projectName) {
router.push(`/projects/${projectName}`);
}
function formatDate(timestamp) {
@@ -47,76 +48,37 @@ function formatSteps(steps) {
</div>
<div v-if="loading" class="text-center py-12">
<div class="text-gray-500 dark:text-gray-400">Loading experiments...</div>
<div class="text-gray-500 dark:text-gray-400">Loading projects...</div>
</div>
<div
v-else-if="experiments.length === 0"
v-else-if="projects.length === 0"
class="bg-white dark:bg-gray-900 rounded-lg shadow-md p-8 text-center border border-gray-200 dark:border-gray-800"
>
<div class="text-gray-500 dark:text-gray-400 mb-4">No boards found</div>
<div class="text-gray-500 dark:text-gray-400 mb-4">No projects found</div>
<p class="text-sm text-gray-400 dark:text-gray-500">
Start tracking your ML experiments with KohakuBoard client library.
<br />
Boards are automatically discovered from:
<code class="bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded"
>./kohakuboard</code
>
</p>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="experiment in experiments"
:key="experiment.id"
@click="viewExperiment(experiment.id)"
class="bg-white dark:bg-gray-900 rounded-lg shadow-md p-4 cursor-pointer hover:shadow-lg transition-all border border-gray-200 dark:border-gray-800 hover:border-blue-400 dark:hover:border-blue-600"
v-for="project in projects"
:key="project.name"
@click="viewProject(project.name)"
class="bg-white dark:bg-gray-900 rounded-lg shadow-md p-6 cursor-pointer hover:shadow-lg transition-all border border-gray-200 dark:border-gray-800 hover:border-blue-400 dark:hover:border-blue-600"
>
<div class="flex items-start justify-between mb-3">
<h3 class="font-semibold text-lg text-gray-900 dark:text-gray-100">
{{ experiment.name }}
</h3>
<span
class="px-2 py-1 text-xs rounded font-medium"
:class="{
'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400':
experiment.status === 'completed',
'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400':
experiment.status === 'running',
'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400':
experiment.status === 'stopped',
}"
>
{{ experiment.status }}
</span>
<h3 class="font-semibold text-xl text-gray-900 dark:text-gray-100 mb-2">
{{ project.display_name }}
</h3>
<div class="text-gray-600 dark:text-gray-400 mb-4">
{{ project.run_count }} {{ project.run_count === 1 ? "run" : "runs" }}
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
{{ experiment.description }}
</p>
<div class="grid grid-cols-2 gap-2 text-sm">
<div>
<div class="text-gray-500 dark:text-gray-500">Board ID</div>
<div
class="font-mono text-xs text-gray-900 dark:text-gray-100 truncate"
:title="experiment.id"
>
{{ experiment.id }}
</div>
</div>
<div>
<div class="text-gray-500 dark:text-gray-500">Status</div>
<div class="font-medium text-gray-900 dark:text-gray-100">
{{ experiment.status }}
</div>
</div>
</div>
<div
class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-800 text-xs text-gray-500 dark:text-gray-500"
v-if="project.updated_at"
class="text-xs text-gray-500 dark:text-gray-500"
>
Created: {{ formatDate(experiment.created_at) }}
Updated: {{ formatDate(project.updated_at) }}
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,140 @@
<script setup>
import { ref, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { fetchProjectRuns } from "@/utils/api";
const route = useRoute();
const router = useRouter();
const project = ref(route.params.project);
const runs = ref([]);
const loading = ref(true);
const projectOwner = ref(null);
onMounted(async () => {
try {
const result = await fetchProjectRuns(project.value);
runs.value = result.runs;
projectOwner.value = result.owner;
} catch (error) {
console.error("Failed to fetch runs:", error);
} finally {
loading.value = false;
}
});
function viewRun(runId) {
// Use the main experiments viewer (the most important page!)
router.push(`/experiments/${runId}`);
}
function formatDate(timestamp) {
if (!timestamp) return "N/A";
return new Date(timestamp).toLocaleString();
}
function formatSize(bytes) {
if (!bytes) return "N/A";
const mb = bytes / 1024 / 1024;
if (mb < 1) return `${(bytes / 1024).toFixed(1)} KB`;
if (mb < 1024) return `${mb.toFixed(1)} MB`;
return `${(mb / 1024).toFixed(2)} GB`;
}
</script>
<template>
<div>
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
{{ project }}
</h1>
<p
v-if="projectOwner"
class="text-sm text-gray-500 dark:text-gray-400 mt-1"
>
Owner: {{ projectOwner }}
</p>
</div>
<router-link
to="/projects"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 dark:bg-gray-500 dark:hover:bg-gray-600 text-white rounded-md font-medium transition-colors shadow-sm"
>
Back to Projects
</router-link>
</div>
<div v-if="loading" class="text-center py-12">
<div class="text-gray-500 dark:text-gray-400">Loading runs...</div>
</div>
<div
v-else-if="runs.length === 0"
class="bg-white dark:bg-gray-900 rounded-lg shadow-md p-8 text-center border border-gray-200 dark:border-gray-800"
>
<div class="text-gray-500 dark:text-gray-400 mb-4">No runs found</div>
<p class="text-sm text-gray-400 dark:text-gray-500">
This project doesn't have any training runs yet.
</p>
</div>
<div v-else class="grid grid-cols-1 gap-4">
<div
v-for="run in runs"
:key="run.run_id"
@click="viewRun(run.run_id)"
class="bg-white dark:bg-gray-900 rounded-lg shadow-md p-4 cursor-pointer hover:shadow-lg transition-all border border-gray-200 dark:border-gray-800 hover:border-blue-400 dark:hover:border-blue-600"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<h3 class="font-semibold text-lg text-gray-900 dark:text-gray-100">
{{ run.name }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400 font-mono mt-1">
{{ run.run_id }}
</p>
</div>
<div v-if="run.private !== undefined" class="ml-4">
<span
class="px-2 py-1 text-xs rounded font-medium"
:class="{
'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400':
run.private,
'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400':
!run.private,
}"
>
{{ run.private ? "Private" : "Public" }}
</span>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4 text-sm">
<div>
<div class="text-gray-500 dark:text-gray-500">Created</div>
<div class="text-gray-900 dark:text-gray-100">
{{ formatDate(run.created_at) }}
</div>
</div>
<div v-if="run.last_synced_at">
<div class="text-gray-500 dark:text-gray-500">Last Synced</div>
<div class="text-gray-900 dark:text-gray-100">
{{ formatDate(run.last_synced_at) }}
</div>
</div>
<div v-if="run.total_size">
<div class="text-gray-500 dark:text-gray-500">Size</div>
<div class="text-gray-900 dark:text-gray-100">
{{ formatSize(run.total_size) }}
</div>
</div>
<div v-if="run.config && Object.keys(run.config).length > 0">
<div class="text-gray-500 dark:text-gray-500">Config</div>
<div class="text-gray-900 dark:text-gray-100 text-xs">
{{ Object.keys(run.config).length }} params
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,75 @@
<script setup>
import { ref, onMounted } from "vue";
import { useRouter } from "vue-router";
import { fetchProjects } from "@/utils/api";
const router = useRouter();
const projects = ref([]);
const loading = ref(true);
onMounted(async () => {
try {
const result = await fetchProjects();
projects.value = result.projects;
} catch (error) {
console.error("Failed to fetch projects:", error);
} finally {
loading.value = false;
}
});
function viewProject(projectName) {
router.push(`/projects/${projectName}`);
}
function formatDate(timestamp) {
if (!timestamp) return "N/A";
return new Date(timestamp).toLocaleString();
}
</script>
<template>
<div>
<div class="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
Projects
</h1>
</div>
<div v-if="loading" class="text-center py-12">
<div class="text-gray-500 dark:text-gray-400">Loading projects...</div>
</div>
<div
v-else-if="projects.length === 0"
class="bg-white dark:bg-gray-900 rounded-lg shadow-md p-8 text-center border border-gray-200 dark:border-gray-800"
>
<div class="text-gray-500 dark:text-gray-400 mb-4">No projects found</div>
<p class="text-sm text-gray-400 dark:text-gray-500">
Start tracking your ML experiments with KohakuBoard client library.
</p>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="project in projects"
:key="project.name"
@click="viewProject(project.name)"
class="bg-white dark:bg-gray-900 rounded-lg shadow-md p-6 cursor-pointer hover:shadow-lg transition-all border border-gray-200 dark:border-gray-800 hover:border-blue-400 dark:hover:border-blue-600"
>
<h3 class="font-semibold text-xl text-gray-900 dark:text-gray-100 mb-2">
{{ project.display_name }}
</h3>
<div class="text-gray-600 dark:text-gray-400 mb-4">
{{ project.run_count }} {{ project.run_count === 1 ? "run" : "runs" }}
</div>
<div
v-if="project.updated_at"
class="text-xs text-gray-500 dark:text-gray-500"
>
Updated: {{ formatDate(project.updated_at) }}
</div>
</div>
</div>
</div>
</template>

View File

@@ -22,6 +22,9 @@ declare module 'vue-router/auto-routes' {
'/dashboard/[id]': RouteRecordInfo<'/dashboard/[id]', '/dashboard/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
'/experiments/': RouteRecordInfo<'/experiments/', '/experiments', Record<never, never>, Record<never, never>>,
'/experiments/[id]': RouteRecordInfo<'/experiments/[id]', '/experiments/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
'/projects/': RouteRecordInfo<'/projects/', '/projects', Record<never, never>, Record<never, never>>,
'/projects/[project]/': RouteRecordInfo<'/projects/[project]/', '/projects/:project', { project: ParamValue<true> }, { project: ParamValue<false> }>,
'/projects/[project]/[id]': RouteRecordInfo<'/projects/[project]/[id]', '/projects/:project/:id', { project: ParamValue<true>, id: ParamValue<true> }, { project: ParamValue<false>, id: ParamValue<false> }>,
}
/**
@@ -51,6 +54,18 @@ declare module 'vue-router/auto-routes' {
routes: '/experiments/[id]'
views: never
}
'src/pages/projects/index.vue': {
routes: '/projects/'
views: never
}
'src/pages/projects/[project]/index.vue': {
routes: '/projects/[project]/'
views: never
}
'src/pages/projects/[project]/[id].vue': {
routes: '/projects/[project]/[id]'
views: never
}
}
/**

View File

@@ -74,7 +74,43 @@ export async function generateMockData(config) {
}
// ============================================================================
// BOARDS API - Real data from file system
// SYSTEM API - Mode detection
// ============================================================================
/**
* Get system information (mode, auth requirements, user info)
* @returns {Promise<Object>} System info
*/
export async function getSystemInfo() {
const response = await api.get("/system/info");
return response.data;
}
// ============================================================================
// PROJECTS API - Project and run management
// ============================================================================
/**
* Fetch all accessible projects
* @returns {Promise<Object>} Projects list
*/
export async function fetchProjects() {
const response = await api.get("/projects");
return response.data;
}
/**
* Fetch runs in a project
* @param {string} projectName - Project name
* @returns {Promise<Object>} Runs list with project info
*/
export async function fetchProjectRuns(projectName) {
const response = await api.get(`/projects/${projectName}/runs`);
return response.data;
}
// ============================================================================
// BOARDS API - Real data from file system (LEGACY - kept for compatibility)
// ============================================================================
/**
@@ -106,6 +142,119 @@ export async function fetchBoardSummary(boardId) {
return response.data;
}
// ============================================================================
// RUN DATA API - Project-based run access
// ============================================================================
/**
* Fetch run summary
* @param {string} project - Project name
* @param {string} runId - Run ID
* @returns {Promise<Object>} Run summary
*/
export async function fetchRunSummary(project, runId) {
const response = await api.get(`/projects/${project}/runs/${runId}/summary`);
return response.data;
}
/**
* Fetch available scalar metrics for a run
* @param {string} project - Project name
* @param {string} runId - Run ID
* @returns {Promise<Object>} Object with metrics array
*/
export async function fetchRunScalars(project, runId) {
const response = await api.get(`/projects/${project}/runs/${runId}/scalars`);
return response.data;
}
/**
* Fetch scalar data for a specific metric
* @param {string} project - Project name
* @param {string} runId - Run ID
* @param {string} metric - Metric name
* @param {Object} params - Query parameters (limit, etc.)
* @returns {Promise<Object>} Object with metric name and data array
*/
export async function fetchRunScalarData(project, runId, metric, params = {}) {
const response = await api.get(
`/projects/${project}/runs/${runId}/scalars/${metric}`,
{
params,
},
);
return response.data;
}
/**
* Fetch available media log names
* @param {string} project - Project name
* @param {string} runId - Run ID
* @returns {Promise<Object>} Object with media array
*/
export async function fetchRunMedia(project, runId) {
const response = await api.get(`/projects/${project}/runs/${runId}/media`);
return response.data;
}
/**
* Fetch media data for a specific log name
* @param {string} project - Project name
* @param {string} runId - Run ID
* @param {string} name - Media log name
* @param {Object} params - Query parameters (limit, etc.)
* @returns {Promise<Object>} Object with name and data array
*/
export async function fetchRunMediaData(project, runId, name, params = {}) {
const response = await api.get(
`/projects/${project}/runs/${runId}/media/${name}`,
{
params,
},
);
return response.data;
}
/**
* Get media file URL
* @param {string} project - Project name
* @param {string} runId - Run ID
* @param {string} filename - Media filename
* @returns {string} Media file URL
*/
export function getRunMediaFileUrl(project, runId, filename) {
return `/api/projects/${project}/runs/${runId}/media/files/${filename}`;
}
/**
* Fetch available table log names
* @param {string} project - Project name
* @param {string} runId - Run ID
* @returns {Promise<Object>} Object with tables array
*/
export async function fetchRunTables(project, runId) {
const response = await api.get(`/projects/${project}/runs/${runId}/tables`);
return response.data;
}
/**
* Fetch table data for a specific log name
* @param {string} project - Project name
* @param {string} runId - Run ID
* @param {string} name - Table log name
* @param {Object} params - Query parameters (limit, etc.)
* @returns {Promise<Object>} Object with name and data array
*/
export async function fetchRunTableData(project, runId, name, params = {}) {
const response = await api.get(
`/projects/${project}/runs/${runId}/tables/${name}`,
{
params,
},
);
return response.data;
}
/**
* Fetch available scalar metrics for a board
* @param {string} boardId - Board identifier