From 0600979fb14bc58aa715f54af0786ebaf4448d8b Mon Sep 17 00:00:00 2001 From: nasamio Date: Tue, 11 Aug 2026 15:30:57 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ai-providers):=20add=20homepag?= =?UTF-8?q?e=20quick-link=20icon=20to=20provider=20base=20url?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProviderTable/ProviderTable.module.scss | 30 +++++++++++++++++- .../ProviderTable/ProviderTable.test.tsx | 31 +++++++++++++++++++ .../providers/ProviderTable/ProviderTable.tsx | 24 +++++++++++++- .../providers/ProviderTable/homepage.test.ts | 26 ++++++++++++++++ .../providers/ProviderTable/homepage.ts | 12 +++++++ apps/web/src/i18n/locales/en.json | 1 + apps/web/src/i18n/locales/zh-CN.json | 1 + apps/web/src/i18n/locales/zh-TW.json | 1 + 8 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/providers/ProviderTable/homepage.test.ts create mode 100644 apps/web/src/components/providers/ProviderTable/homepage.ts diff --git a/apps/web/src/components/providers/ProviderTable/ProviderTable.module.scss b/apps/web/src/components/providers/ProviderTable/ProviderTable.module.scss index ce35296e4..43e15d1f6 100644 --- a/apps/web/src/components/providers/ProviderTable/ProviderTable.module.scss +++ b/apps/web/src/components/providers/ProviderTable/ProviderTable.module.scss @@ -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; @@ -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; diff --git a/apps/web/src/components/providers/ProviderTable/ProviderTable.test.tsx b/apps/web/src/components/providers/ProviderTable/ProviderTable.test.tsx index 2b1488bcd..5ac71deb0 100644 --- a/apps/web/src/components/providers/ProviderTable/ProviderTable.test.tsx +++ b/apps/web/src/components/providers/ProviderTable/ProviderTable.test.tsx @@ -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); + } + }); }); diff --git a/apps/web/src/components/providers/ProviderTable/ProviderTable.tsx b/apps/web/src/components/providers/ProviderTable/ProviderTable.tsx index e63869e5e..5adfff701 100644 --- a/apps/web/src/components/providers/ProviderTable/ProviderTable.tsx +++ b/apps/web/src/components/providers/ProviderTable/ProviderTable.tsx @@ -12,6 +12,7 @@ import { Button } from '@/components/ui/Button'; import { ToggleSwitch } from '@/components/ui/ToggleSwitch'; import { IconCheck, + IconExternalLink, IconEye, IconPencil, IconTrash2, @@ -19,6 +20,7 @@ import { } 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'; @@ -211,6 +213,7 @@ export function ProviderTable({ {rows.map((row) => { const kindLabel = PROVIDER_KIND_LABELS[row.kind]; + const homepageUrl = resolveProviderHomepage(row.baseUrl); return (
- {row.baseUrl || '—'} + {row.baseUrl ? ( + <> + {homepageUrl && ( + + + + )} + {row.baseUrl} + + ) : ( + '—' + )}
diff --git a/apps/web/src/components/providers/ProviderTable/homepage.test.ts b/apps/web/src/components/providers/ProviderTable/homepage.test.ts new file mode 100644 index 000000000..46b3c2c91 --- /dev/null +++ b/apps/web/src/components/providers/ProviderTable/homepage.test.ts @@ -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(); + }); +}); diff --git a/apps/web/src/components/providers/ProviderTable/homepage.ts b/apps/web/src/components/providers/ProviderTable/homepage.ts new file mode 100644 index 000000000..a3c8c0129 --- /dev/null +++ b/apps/web/src/components/providers/ProviderTable/homepage.ts @@ -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; + } +}; diff --git a/apps/web/src/i18n/locales/en.json b/apps/web/src/i18n/locales/en.json index fa9816f0c..cea6df675 100644 --- a/apps/web/src/i18n/locales/en.json +++ b/apps/web/src/i18n/locales/en.json @@ -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", diff --git a/apps/web/src/i18n/locales/zh-CN.json b/apps/web/src/i18n/locales/zh-CN.json index 7fbbe8a12..d345ac3b9 100644 --- a/apps/web/src/i18n/locales/zh-CN.json +++ b/apps/web/src/i18n/locales/zh-CN.json @@ -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": "点击右上角“添加配置”创建第一个配置", diff --git a/apps/web/src/i18n/locales/zh-TW.json b/apps/web/src/i18n/locales/zh-TW.json index f8f54f519..e44409c3e 100644 --- a/apps/web/src/i18n/locales/zh-TW.json +++ b/apps/web/src/i18n/locales/zh-TW.json @@ -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": "點擊右上角「新增設定」建立第一個設定",