feat: add adapter documentation link button

Add 'View Docs' button that links to the corresponding adapter's
documentation page via link.langbot.app short links.

Appears in:
- Wizard adapter selection cards (Step 0)
- Wizard bot config card header (Step 1)
- Bot create/edit form (adapter config section)

Supports all 7 languages (en/zh-Hans/zh-Hant/ja/th/vi/es).
Doc links auto-resolve to the correct language based on UI locale.
This commit is contained in:
RockChinQ
2026-03-30 16:06:54 +08:00
parent 6bf6deaefd
commit 921d12f596
10 changed files with 124 additions and 5 deletions

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useMemo, useRef, useState } from 'react';
import i18n from 'i18next';
import { import {
IChooseAdapterEntity, IChooseAdapterEntity,
IPipelineEntity, IPipelineEntity,
@@ -13,6 +14,9 @@ import { UUID } from 'uuidjs';
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent'; import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
import { Bot } from '@/app/infra/entities/api'; import { Bot } from '@/app/infra/entities/api';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
import { ExternalLink } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
@@ -528,6 +532,30 @@ export default function BotForm({
{adapterDescriptionList[currentAdapter]} {adapterDescriptionList[currentAdapter]}
</FormDescription> </FormDescription>
)} )}
{currentAdapter &&
(() => {
const docUrl = getAdapterDocUrl(
currentAdapter,
i18n.language,
);
return docUrl ? (
<Button
variant="link"
size="sm"
className="h-auto p-0 text-xs"
asChild
>
<a
href={docUrl}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="mr-1 h-3 w-3" />
{t('bots.viewAdapterDocs')}
</a>
</Button>
) : null;
})()}
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

@@ -0,0 +1,43 @@
/**
* Returns the documentation URL for a given adapter name,
* using link.langbot.app short links.
*/
export function getAdapterDocUrl(
adapterName: string,
locale: string,
): string | null {
// Map locale to doc language prefix
let lang: string;
if (locale.startsWith('zh')) {
lang = 'zh';
} else if (locale.startsWith('ja')) {
lang = 'ja';
} else {
lang = 'en';
}
// Only adapters with dedicated doc pages
const ADAPTER_DOC_SLUGS: Record<string, string> = {
telegram: 'telegram',
discord: 'discord',
slack: 'slack',
line: 'line',
kook: 'kook',
lark: 'lark',
dingtalk: 'dingtalk',
aiocqhttp: 'aiocqhttp',
qqofficial: 'qqofficial',
wecom: 'wecom',
wecomcs: 'wecomcs',
wecombot: 'wecombot',
officialaccount: 'officialaccount',
wechatpad: 'wechatpad',
openclaw_weixin: 'openclaw_weixin',
satori: 'satori',
};
const slug = ADAPTER_DOC_SLUGS[adapterName];
if (!slug) return null;
return `https://link.langbot.app/${lang}/platforms/${slug}`;
}

View File

@@ -13,6 +13,7 @@ import {
PartyPopper, PartyPopper,
Loader2, Loader2,
X, X,
ExternalLink,
} from 'lucide-react'; } from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
@@ -45,6 +46,8 @@ import {
groupByCategory, groupByCategory,
getCategoryLabel, getCategoryLabel,
} from '@/app/infra/entities/adapter-categories'; } from '@/app/infra/entities/adapter-categories';
import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
import i18n from 'i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@@ -798,6 +801,24 @@ function StepPlatform({
<p className="text-sm text-muted-foreground line-clamp-2"> <p className="text-sm text-muted-foreground line-clamp-2">
{extractI18nObject(adapter.description)} {extractI18nObject(adapter.description)}
</p> </p>
{(() => {
const docUrl = getAdapterDocUrl(
adapter.name,
i18n.language,
);
return docUrl ? (
<a
href={docUrl}
target="_blank"
rel="noopener noreferrer"
className="mt-2 inline-flex items-center text-xs text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
<ExternalLink className="mr-1 h-3 w-3" />
{t('bots.viewAdapterDocs')}
</a>
) : null;
})()}
</CardContent> </CardContent>
</Card> </Card>
))} ))}
@@ -867,11 +888,31 @@ function StepBotConfig({
{adapterConfigItems.length > 0 && ( {adapterConfigItems.length > 0 && (
<Card> <Card>
<CardHeader className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2"> <CardHeader className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
<div className="flex items-center gap-2">
<CardTitle className="text-base"> <CardTitle className="text-base">
{t('wizard.config.platformConfig', { {t('wizard.config.platformConfig', {
platform: adapterLabel, platform: adapterLabel,
})} })}
</CardTitle> </CardTitle>
{selectedAdapterName &&
(() => {
const docUrl = getAdapterDocUrl(
selectedAdapterName,
i18n.language,
);
return docUrl ? (
<a
href={docUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-xs text-primary hover:underline"
>
<ExternalLink className="mr-1 h-3 w-3" />
{t('bots.viewAdapterDocs')}
</a>
) : null;
})()}
</div>
<Button <Button
size="sm" size="sm"
onClick={onSaveBot} onClick={onSaveBot}

View File

@@ -288,6 +288,7 @@ const enUS = {
platformAdapter: 'Platform/Adapter Selection', platformAdapter: 'Platform/Adapter Selection',
selectAdapter: 'Select Adapter', selectAdapter: 'Select Adapter',
adapterConfig: 'Adapter Configuration', adapterConfig: 'Adapter Configuration',
viewAdapterDocs: 'View Docs',
bindPipeline: 'Bind Pipeline', bindPipeline: 'Bind Pipeline',
selectPipeline: 'Select Pipeline', selectPipeline: 'Select Pipeline',
selectBot: 'Select Bot', selectBot: 'Select Bot',

View File

@@ -297,6 +297,7 @@ const esES = {
platformAdapter: 'Selección de plataforma/adaptador', platformAdapter: 'Selección de plataforma/adaptador',
selectAdapter: 'Seleccionar adaptador', selectAdapter: 'Seleccionar adaptador',
adapterConfig: 'Configuración del adaptador', adapterConfig: 'Configuración del adaptador',
viewAdapterDocs: 'Ver documentación',
bindPipeline: 'Vincular Pipeline', bindPipeline: 'Vincular Pipeline',
selectPipeline: 'Seleccionar Pipeline', selectPipeline: 'Seleccionar Pipeline',
selectBot: 'Seleccionar Bot', selectBot: 'Seleccionar Bot',

View File

@@ -293,6 +293,7 @@
platformAdapter: 'プラットフォーム/アダプター選択', platformAdapter: 'プラットフォーム/アダプター選択',
selectAdapter: 'アダプターを選択', selectAdapter: 'アダプターを選択',
adapterConfig: 'アダプター設定', adapterConfig: 'アダプター設定',
viewAdapterDocs: 'ドキュメントを見る',
bindPipeline: 'パイプラインを紐付け', bindPipeline: 'パイプラインを紐付け',
selectPipeline: 'パイプラインを選択', selectPipeline: 'パイプラインを選択',
selectBot: 'ボットを選択してください', selectBot: 'ボットを選択してください',

View File

@@ -283,6 +283,7 @@ const thTH = {
platformAdapter: 'การเลือกแพลตฟอร์ม/อะแดปเตอร์', platformAdapter: 'การเลือกแพลตฟอร์ม/อะแดปเตอร์',
selectAdapter: 'เลือกอะแดปเตอร์', selectAdapter: 'เลือกอะแดปเตอร์',
adapterConfig: 'การกำหนดค่าอะแดปเตอร์', adapterConfig: 'การกำหนดค่าอะแดปเตอร์',
viewAdapterDocs: 'ดูเอกสาร',
bindPipeline: 'ผูก Pipeline', bindPipeline: 'ผูก Pipeline',
selectPipeline: 'เลือก Pipeline', selectPipeline: 'เลือก Pipeline',
selectBot: 'เลือก Bot', selectBot: 'เลือก Bot',

View File

@@ -292,6 +292,7 @@ const viVN = {
platformAdapter: 'Nền tảng/Lựa chọn Adapter', platformAdapter: 'Nền tảng/Lựa chọn Adapter',
selectAdapter: 'Chọn Adapter', selectAdapter: 'Chọn Adapter',
adapterConfig: 'Cấu hình Adapter', adapterConfig: 'Cấu hình Adapter',
viewAdapterDocs: 'Xem tài liệu',
bindPipeline: 'Liên kết Pipeline', bindPipeline: 'Liên kết Pipeline',
selectPipeline: 'Chọn Pipeline', selectPipeline: 'Chọn Pipeline',
selectBot: 'Chọn Bot', selectBot: 'Chọn Bot',

View File

@@ -276,6 +276,7 @@ const zhHans = {
platformAdapter: '平台/适配器选择', platformAdapter: '平台/适配器选择',
selectAdapter: '选择适配器', selectAdapter: '选择适配器',
adapterConfig: '适配器配置', adapterConfig: '适配器配置',
viewAdapterDocs: '查看文档',
bindPipeline: '绑定流水线', bindPipeline: '绑定流水线',
selectPipeline: '选择流水线', selectPipeline: '选择流水线',
selectBot: '请选择机器人', selectBot: '请选择机器人',

View File

@@ -275,6 +275,7 @@ const zhHant = {
platformAdapter: '平台/適配器選擇', platformAdapter: '平台/適配器選擇',
selectAdapter: '選擇適配器', selectAdapter: '選擇適配器',
adapterConfig: '適配器設定', adapterConfig: '適配器設定',
viewAdapterDocs: '查看文檔',
bindPipeline: '綁定流程線', bindPipeline: '綁定流程線',
selectPipeline: '選擇流程線', selectPipeline: '選擇流程線',
selectBot: '請選擇機器人', selectBot: '請選擇機器人',