@@ -169,3 +169,124 @@ describe('AI chart metric semantics', () => {
169169 } ) ;
170170 } ) ;
171171} ) ;
172+
173+ describe ( 'AI chart Chinese workflow' , ( ) => {
174+ it ( 'requests Chinese presentation copy and renders the returned chart and summary' , ( ) => {
175+ cy . viewport ( 1440 , 900 ) ;
176+ cy . fixture < FixtureRow [ ] > ( 'api/benchmarks.json' ) . then ( ( fixtureRows ) => {
177+ const rows = [
178+ fixtureRow ( fixtureRows , 'b200' , 40 , 12_000 ) ,
179+ fixtureRow ( fixtureRows , 'mi355x' , 40 , 10_000 ) ,
180+ ] ;
181+ const spec = benchmarkSpec ( {
182+ title : '每芯片吞吐量对比' ,
183+ description : '在目标交互性下对比两种芯片。' ,
184+ yAxisLabel : '每芯片吞吐量' ,
185+ } ) ;
186+
187+ cy . intercept ( 'GET' , '**/api/v1/benchmarks?*' , rows ) . as ( 'zhBenchmarks' ) ;
188+ cy . intercept ( 'POST' , 'https://api.openai.com/v1/chat/completions' , ( request ) => {
189+ const systemPrompt = request . body . messages ?. [ 0 ] ?. content ?? '' ;
190+ if ( systemPrompt . includes ( 'chart generation assistant' ) ) {
191+ expect ( systemPrompt ) . to . contain ( 'natural Simplified Chinese' ) ;
192+ request . reply ( { choices : [ { message : { content : JSON . stringify ( spec ) } } ] } ) ;
193+ return ;
194+ }
195+
196+ expect ( systemPrompt ) . to . contain ( '用自然、准确的简体中文回答' ) ;
197+ request . reply ( { choices : [ { message : { content : 'B200 在该配置下吞吐量更高。' } } ] } ) ;
198+ } ) . as ( 'zhOpenAi' ) ;
199+
200+ cy . visit ( '/zh/ai-chart' ) ;
201+ cy . get ( 'input[placeholder="OpenAI API Key"]' ) . type ( 'test-api-key' , { log : false } ) ;
202+ cy . get ( 'textarea[placeholder="描述您想查看的图表……"]' ) . type (
203+ '对比 B200 和 MI355X 的每芯片吞吐量' ,
204+ ) ;
205+ cy . contains ( 'button' , '生成图表' ) . click ( ) ;
206+
207+ cy . wait ( '@zhBenchmarks' ) ;
208+ cy . get ( '#ai-chart-bar-export' )
209+ . should ( 'contain.text' , '每芯片吞吐量对比' )
210+ . and ( 'contain.text' , '在目标交互性下对比两种芯片。' )
211+ . and ( 'contain.text' , '每芯片吞吐量' ) ;
212+ cy . get ( '[role="img"][aria-label="AI 生成的条形图"]' ) . should ( 'be.visible' ) ;
213+ cy . contains ( '[data-slot="card-title"]' , 'AI 总结' ) . should ( 'be.visible' ) ;
214+ cy . contains ( 'B200 在该配置下吞吐量更高。' ) . should ( 'be.visible' ) ;
215+ } ) ;
216+ } ) ;
217+
218+ it ( 'shows a localized empty state for one unmatched chart in a multi-chart result' , ( ) => {
219+ cy . fixture < FixtureRow [ ] > ( 'api/benchmarks.json' ) . then ( ( fixtureRows ) => {
220+ const rows = [ fixtureRow ( fixtureRows , 'b200' , 40 , 12_000 ) ] ;
221+ const specs = [
222+ benchmarkSpec ( {
223+ title : 'B200 吞吐量' ,
224+ description : '实测配置。' ,
225+ hardwareKeys : [ 'b200' ] ,
226+ yAxisLabel : '每芯片吞吐量' ,
227+ } ) ,
228+ benchmarkSpec ( {
229+ title : 'H100 吞吐量' ,
230+ description : '当前数据集中没有匹配项。' ,
231+ hardwareKeys : [ 'h100' ] ,
232+ yAxisLabel : '每芯片吞吐量' ,
233+ } ) ,
234+ ] ;
235+
236+ cy . intercept ( 'GET' , '**/api/v1/benchmarks?*' , rows ) . as ( 'multiChartBenchmarks' ) ;
237+ cy . intercept ( 'POST' , 'https://api.openai.com/v1/chat/completions' , ( request ) => {
238+ const systemPrompt = request . body . messages ?. [ 0 ] ?. content ?? '' ;
239+ request . reply ( {
240+ choices : [
241+ {
242+ message : {
243+ content : systemPrompt . includes ( 'chart generation assistant' )
244+ ? JSON . stringify ( specs )
245+ : '已对比可用数据。' ,
246+ } ,
247+ } ,
248+ ] ,
249+ } ) ;
250+ } ) ;
251+
252+ cy . visit ( '/zh/ai-chart' ) ;
253+ cy . get ( 'input[placeholder="OpenAI API Key"]' ) . type ( 'test-api-key' , { log : false } ) ;
254+ cy . get ( 'textarea' ) . type ( '生成两张吞吐量图表' ) ;
255+ cy . contains ( 'button' , '生成图表' ) . click ( ) ;
256+
257+ cy . wait ( '@multiChartBenchmarks' ) ;
258+ cy . contains ( 'H100 吞吐量' )
259+ . closest ( '[data-slot="card"]' )
260+ . should ( 'contain.text' , '没有数据符合这项图表配置。' ) ;
261+ } ) ;
262+ } ) ;
263+
264+ it ( 'does not expose a provider error body or API key on the Chinese route' , ( ) => {
265+ cy . intercept ( 'POST' , 'https://api.openai.com/v1/chat/completions' , {
266+ statusCode : 401 ,
267+ body : { error : { message : 'provider-internal-error sk-sensitive-example-key' } } ,
268+ } ) . as ( 'failedProvider' ) ;
269+ cy . visit ( '/zh/ai-chart' ) ;
270+ cy . get ( 'input[placeholder="OpenAI API Key"]' ) . type ( 'sk-sensitive-example-key' , { log : false } ) ;
271+ cy . get ( 'textarea' ) . type ( '对比吞吐量' ) ;
272+ cy . contains ( 'button' , '生成图表' ) . click ( ) ;
273+ cy . wait ( '@failedProvider' ) ;
274+
275+ cy . contains ( '图表请求失败。请检查 API 密钥和服务商设置后重试。' ) . should ( 'be.visible' ) ;
276+ cy . contains ( 'provider-internal-error' ) . should ( 'not.exist' ) ;
277+ cy . get ( '[data-testid="ai-chart-error"]' ) . should ( 'not.contain.text' , 'sk-sensitive-example-key' ) ;
278+ } ) ;
279+
280+ for ( const width of [ 375 , 390 ] ) {
281+ it ( `keeps Chinese provider controls and examples within ${ width } px` , ( ) => {
282+ cy . viewport ( width , 844 ) ;
283+ cy . visit ( '/zh/ai-chart' ) ;
284+ cy . get ( 'input[placeholder="OpenAI API Key"]' ) . should ( 'be.visible' ) ;
285+ cy . get ( 'textarea[placeholder="描述您想查看的图表……"]' ) . should ( 'be.visible' ) ;
286+ cy . contains ( '示例提示' ) . should ( 'be.visible' ) ;
287+ cy . document ( ) . then ( ( doc ) => {
288+ expect ( doc . documentElement . scrollWidth ) . to . be . lte ( doc . documentElement . clientWidth ) ;
289+ } ) ;
290+ } ) ;
291+ }
292+ } ) ;
0 commit comments