Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/main/lm-studio/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { Exec } from '../exec';
import { MESSAGE_TYPE, MessageData } from '../ipc-data-type';
import { RunResult } from '@podman-desktop/api';
import { loggerFactory } from '../terminal-log';
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
const commandLine = new Exec();

export default async function init(ipcMain: IpcMain) {
Expand Down Expand Up @@ -127,6 +130,28 @@ export default async function init(ipcMain: IpcMain) {
);
}

function resolveModelAbsolutePath(p: string): string {
try {
if (!p) return p;
// If already absolute, normalize and return
if (path.isAbsolute(p)) {
return path.normalize(p);
}
// Default LM Studio models directory under user home
const modelsBaseDir = path.join(os.homedir(), '.lmstudio', 'models');
// lms returns POSIX-style separators sometimes; normalize to current OS
const normalizedRelative = p.replace(/\//g, path.sep);
const absCandidate = path.normalize(path.join(modelsBaseDir, normalizedRelative));
// Only adopt absolute path if it actually exists to avoid misleading display
if (fs.existsSync(absCandidate)) {
return absCandidate;
}
return p;
} catch {
return p;
}
}

async function queryServerStatus() {
const serverStatusResult = await commandLine.exec(
'lms',
Expand Down Expand Up @@ -156,11 +181,26 @@ async function queryModelStatus() {
const downloadedModel = JSON.parse(result.stdout) as LMModel[];
const loadedModel = JSON.parse(result2.stdout) as LMModel[];
for (const model of downloadedModel) {
// Convert model.path to absolute path for UI display
if (model && typeof model.path === 'string') {
model.path = resolveModelAbsolutePath(model.path);
}
if (loadedModel.findIndex((m) => m.modelKey === model.modelKey) >= 0) {
model.isLoaded = true;
} else {
model.isLoaded = false;
}

// 尝试从模型名称中提取参数量信息
if (model.displayName) {
// 匹配类似 "Qwen3 4B" 或 "Gemma 3 27B Instruct" 的模式
const paramMatch = model.displayName.match(/(\d+(?:\.\d+)?)\s*([BM])/i);
if (paramMatch) {
const value = parseFloat(paramMatch[1]);
const unit = paramMatch[2].toUpperCase();
model.parameterCount = unit === 'B' ? value * 1000000000 : value * 1000000;
}
}
}

console.debug('queryModelStatus', result);
Expand Down
1 change: 1 addition & 0 deletions src/main/lm-studio/type-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const modelListSample = {
export type LMModel = typeof modelListSample & {
isLoaded: boolean;
port: number;
parameterCount?: number; // 添加参数量字段
};

const serverStatusExample = { running: true, port: 1234 };
Expand Down
67 changes: 60 additions & 7 deletions src/renderer/pages/lm-service/index.scss
Original file line number Diff line number Diff line change
@@ -1,24 +1,77 @@

.lm-service{
.lm-service {
display: flex;
width: 100%;
justify-content: center;
flex-direction: column;
.lm-service-list{
align-items: center;
height: 100%;

.lm-service-list {
width: calc(100% - 20px);
flex: 0 0 auto;
}
height: 100%;
align-items: center;

.header-container{
.header-container {
display: flex;
justify-content: space-between;
}
}

.lm-studio-demo{
.lm-studio-demo {
width: 575px;
height: 312px;
background-size: contain;
}

.model-list-item {
display: flex;
justify-content: space-between;
align-items: center;

> div:first-child {
flex-grow: 1;
min-width: 0;
}

/* 调整服务地址的容器,并确保它不换行 */
.service-address {
flex-shrink: 0;
margin-left: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 150px;
max-width: 250px;
text-align: right;
}

/* 为列表项操作按钮区域设置固定宽度,避免因按钮数量变化导致的布局问题 */
.ant-list-item-action {
flex-shrink: 0;
width: 300px;
display: flex;
justify-content: flex-end;
gap: 8px;
margin-left: 20px;
}
}

.model-name {
color: #1890ff;
font-weight: 600;
margin-left: 8px; /* 给状态和名称之间添加一些间距 */
}

.service-address {
margin-left: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 150px;
max-width: 250px;
text-align: right;

.service-url {
color: #1890ff; /* 保持超链接的颜色 */
}
}
120 changes: 116 additions & 4 deletions src/renderer/pages/lm-service/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, List, notification, Popconfirm, Typography } from 'antd';
import { Button, List, notification, Popconfirm, Typography, Modal, Descriptions } from 'antd';
import { Link } from 'react-router-dom';
import './index.scss';
import { useState } from 'react';
Expand Down Expand Up @@ -38,6 +38,32 @@ function getState(
return '还未安装';
}

function formatBytes(bytes?: number): string {
if (bytes === undefined || bytes === null) {
return '未知';
}
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

function formatParameterCount(count?: number): string {
if (count === undefined || count === null) {
return '未知';
}
if (count >= 1000000000) {
return (count / 1000000000).toFixed(1) + 'B';
} else if (count >= 1000000) {
return (count / 1000000).toFixed(1) + 'M';
} else if (count >= 1000) {
return (count / 1000).toFixed(1) + 'K';
} else {
return count.toString();
}
}

export default function LMService() {
const { lmServerStatus, lMModels, action, loading, initing } = useLMStudio();
const {
Expand All @@ -61,6 +87,8 @@ export default function LMService() {
serviceName: 'WSL',
actionName: 'install',
});
const [showDetail, setShowDetail] = useState(false);
const [selectedModel, setSelectedModel] = useState<LMModel | null>(null);
// const llmContainer = containers.filter(
// (item) => item.Names.indexOf() >= 0,
// )[0];
Expand Down Expand Up @@ -170,7 +198,36 @@ export default function LMService() {
<List.Item
key={item.serviceName}
actions={[
`http://127.0.0.1:${lmServerStatus.port}`,
(item.state === '已经安装' || item.state === '已经加载') &&
<Button
shape="round"
size="small"
onClick={() => {
const model = lMModels.find(
(m) => m.modelKey === item.name || m.displayName === modelNameDict[item.serviceName]
);
if (model) {
setSelectedModel(model);
} else {
// 创建一个临时的模型对象用于显示
setSelectedModel({
modelKey: item.name,
displayName: modelNameDict[item.serviceName],
isLoaded: item.state === '已经加载',
port: lmServerStatus.port,
type: '',
format: '',
path: '',
sizeBytes: 0,
architecture: '',
maxContextLength: 0,
} as LMModel);
}
setShowDetail(true);
}}
>
详情
</Button>,
item.state === '已经加载' && (
<Button
shape="round"
Expand Down Expand Up @@ -242,9 +299,23 @@ export default function LMService() {
</Button>
),
].filter((button) => button)}
className="model-list-item"
>
<Typography.Text type="success">[{item.state}]</Typography.Text>
{item.name}
<div>
<Typography.Text type={
item.state === '已经加载' ? 'success' :
item.state === '已经安装' ? 'success' :
'secondary'
}>
[{item.state}]
</Typography.Text>
<Typography.Text className="model-name">
{item.name}
</Typography.Text>
</div>
<Typography.Text className="service-address">
服务地址: <span className="service-url">http://127.0.0.1:{lmServerStatus.port}</span>
</Typography.Text>
</List.Item>,
]}
/>
Expand All @@ -254,6 +325,47 @@ export default function LMService() {
rows={3}
style={{ width: 'calc(100% - 20px)' }}
/>
<Modal
open={showDetail}
title="模型信息"
onCancel={() => setShowDetail(false)}
footer={null}
width="70%"
style={{ maxWidth: 720, minWidth: 300 }}
>
<Descriptions bordered column={1} size="small">
<Descriptions.Item label="名称">
{selectedModel?.displayName || selectedModel?.modelKey || '未知'}
</Descriptions.Item>
<Descriptions.Item label="大小">
{formatBytes(selectedModel?.sizeBytes)}
</Descriptions.Item>
<Descriptions.Item label="参数量">
{formatParameterCount(selectedModel?.parameterCount)}
</Descriptions.Item>
<Descriptions.Item label="路径">
{selectedModel?.path || '未知'}
</Descriptions.Item>
<Descriptions.Item label="格式">
{selectedModel?.format || '未知'}
</Descriptions.Item>
{/* <Descriptions.Item label="架构">
{selectedModel?.architecture || '未知'}
</Descriptions.Item> */}
<Descriptions.Item label="上下文长度">
{selectedModel?.maxContextLength ?? '未知'}
</Descriptions.Item>
<Descriptions.Item label="唯一标识">
{selectedModel?.modelKey || '未知'}
</Descriptions.Item>
{/* <Descriptions.Item label="加载状态">
{selectedModel?.isLoaded ? '已加载' : '未加载'}
</Descriptions.Item> */}
<Descriptions.Item label="服务地址">
{`http://127.0.0.1:${lmServerStatus.port}`}
</Descriptions.Item>
</Descriptions>
</Modal>
</div>
);
}