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
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,12 @@ $table-columns: 92px minmax(132px, 0.72fr) minmax(180px, 1fr) 96px 72px minmax(2
}

.cellUrl {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
font-size: 12px;
color: var(--text-secondary);
@include text-ellipsis;

@include provider-card-layout {
grid-area: url;
Expand All @@ -184,6 +187,31 @@ $table-columns: 92px minmax(132px, 0.72fr) minmax(180px, 1fr) 96px 72px minmax(2
}
}

.urlText {
min-width: 0;
@include text-ellipsis;

@include provider-card-layout {
white-space: normal;
overflow-wrap: anywhere;
}
}

.urlLink {
flex-shrink: 0;
display: inline-flex;
align-items: center;
padding: 2px;
color: var(--text-secondary);
border-radius: $radius-sm;
transition: color $transition-fast, background $transition-fast;

&:hover {
color: var(--text-primary);
background: var(--surface-subtle);
}
}

.cellNumeric {
text-align: right;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,4 +378,35 @@ describe('ProviderTable', () => {
expect(getText(renderedRows[1])).toContain('status_bar.no_requests');
expect(renderedRows[1].findAllByType(ProviderStatusBar)).toHaveLength(0);
});

it('renders an external homepage link derived from the base url', () => {
const rows = buildProviderRows({
...emptyInput,
codex: [{ apiKey: 'k1', baseUrl: 'https://low.example.com/v1' }],
});
const renderer = renderTable(rows);

const link = getRows(renderer)[0]
.findAll((node) => node.type === 'a')
.find((node) => node.props['aria-label'] === 'ai_providers.open_homepage');
expect(link).toBeTruthy();
expect(link?.props.href).toBe('https://low.example.com');
expect(link?.props.target).toBe('_blank');
expect(link?.props.rel).toBe('noopener noreferrer');
});

it('skips the homepage link when the base url is missing or malformed', () => {
const rows = buildProviderRows({
...emptyInput,
codex: [
{ apiKey: 'k2', baseUrl: '' },
{ apiKey: 'k3', baseUrl: 'https://' },
],
});
const renderer = renderTable(rows);

for (const row of getRows(renderer)) {
expect(row.findAll((node) => node.type === 'a')).toHaveLength(0);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import { Button } from '@/components/ui/Button';
import { ToggleSwitch } from '@/components/ui/ToggleSwitch';
import {
IconCheck,
IconExternalLink,
IconEye,
IconPencil,
IconTrash2,
IconX,
} from '@/components/ui/icons';
import { ProviderStatusBar } from '../ProviderStatusBar';
import { getProviderKindIcon, PROVIDER_KIND_LABELS } from './kindMeta';
import { resolveProviderHomepage } from './homepage';
import type { ProviderRow } from './rowData';
import styles from './ProviderTable.module.scss';

Expand Down Expand Up @@ -211,6 +213,7 @@ export function ProviderTable({

{rows.map((row) => {
const kindLabel = PROVIDER_KIND_LABELS[row.kind];
const homepageUrl = resolveProviderHomepage(row.baseUrl);
return (
<div
key={row.key}
Expand Down Expand Up @@ -252,7 +255,26 @@ export function ProviderTable({
</div>

<div className={styles.cellUrl} role="cell" title={row.baseUrl || undefined}>
{row.baseUrl || '—'}
{row.baseUrl ? (
<>
{homepageUrl && (
<a
className={styles.urlLink}
href={homepageUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={t('ai_providers.open_homepage')}
title={t('ai_providers.open_homepage')}
onClick={stopPropagation}
>
<IconExternalLink size={13} />
</a>
)}
<span className={styles.urlText}>{row.baseUrl}</span>
</>
) : (
'—'
)}
</div>

<div className={`${styles.cellModels} ${styles.cellNumeric}`} role="cell">
Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/components/providers/ProviderTable/homepage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { resolveProviderHomepage } from './homepage';

describe('resolveProviderHomepage', () => {
it('strips the path and keeps scheme and host', () => {
expect(resolveProviderHomepage('https://sharedchat.cc/v1')).toBe('https://sharedchat.cc');
expect(resolveProviderHomepage('https://api.example.com')).toBe('https://api.example.com');
expect(resolveProviderHomepage('https://example.com/api/chat')).toBe('https://example.com');
});

it('keeps ports and ip addresses', () => {
expect(resolveProviderHomepage('http://127.0.0.1:8317')).toBe('http://127.0.0.1:8317');
expect(resolveProviderHomepage('https://localhost:8443/v1')).toBe('https://localhost:8443');
});

it('assumes https when the scheme is missing', () => {
expect(resolveProviderHomepage('sharedchat.cc/v1')).toBe('https://sharedchat.cc');
});

it('returns null for empty or malformed urls', () => {
expect(resolveProviderHomepage('')).toBeNull();
expect(resolveProviderHomepage(' ')).toBeNull();
expect(resolveProviderHomepage('https://')).toBeNull();
expect(resolveProviderHomepage('not a url at all')).toBeNull();
});
});
12 changes: 12 additions & 0 deletions apps/web/src/components/providers/ProviderTable/homepage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;

export const resolveProviderHomepage = (baseUrl: string): string | null => {
const trimmed = baseUrl.trim();
if (!trimmed) return null;
const withScheme = SCHEME_RE.test(trimmed) ? trimmed : `https://${trimmed}`;
try {
return new URL(withScheme).origin || null;
} catch {
return null;
}
};
1 change: 1 addition & 0 deletions apps/web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,7 @@
"add_config_menu_aria": "Choose a provider type to add",
"row_actions_aria": "More actions",
"detail_button": "Details",
"open_homepage": "Open homepage",
"detail_section_headers": "Custom Headers",
"table_empty_title": "No provider configurations",
"table_empty_desc": "Click \"Add Configuration\" in the top-right corner to create your first one",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,7 @@
"add_config_menu_aria": "选择要添加的提供商类型",
"row_actions_aria": "更多操作",
"detail_button": "详情",
"open_homepage": "访问主站",
"detail_section_headers": "自定义 Header",
"table_empty_title": "暂无提供商配置",
"table_empty_desc": "点击右上角“添加配置”创建第一个配置",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/i18n/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,7 @@
"add_config_menu_aria": "選擇要新增的提供商類型",
"row_actions_aria": "更多操作",
"detail_button": "詳情",
"open_homepage": "訪問主站",
"detail_section_headers": "自訂 Header",
"table_empty_title": "暫無提供商設定",
"table_empty_desc": "點擊右上角「新增設定」建立第一個設定",
Expand Down
Loading