先看看一个概念:网络流,平时可能用不到。流是一种用于访问数据的数据结构,比如说:文件、接口返回的数据等等。
使用流有两个好处:
在网络流中,一个chunk通常是:
网络流主要有三种:
本文中只会使用到ReadableStream和TransformStream。
interfaceReadableStream{getReader():ReadableStreamDefaultReader;pipeThrough(transform:ReadableWritablePair,options:StreamPipeOptions):ReadableStream;readonlylocked:boolean;//忽略//[Symbol.asyncIterator]():AsyncIterator;//cancel(reason:any):Promise;//pipeTo(//destination:WritableStream,//options:StreamPipeOptions//):Promise;//tee():[ReadableStream,ReadableStream];}这三个属性的作用是:
下面来看看getReader()的返回类型:
interfaceReadableStreamGenericReader{cancel(reason:any):Promise;//忽略//readonlyclosed:Promise;}interfaceReadableStreamDefaultReaderextendsReadableStreamGenericReader{releaseLock():void;read():Promise>;}interfaceReadableStreamReadResult{done:boolean;value:TChunk|undefined;}师傅,别念api了,再念人都要傻了,赶紧来一个demo吧。
以下是通过getReader方式来读取ReadableStream的小例子。
constreader=readableStream.getReader();//(A)console.log(readableStream.locked);//true(B)letresult='';try{while(true){const{done,value}=awaitreader.read();//(C)if(done){break;}result+=value;//(D)}}finally{reader.releaseLock();//(E)}console.log('result',result);通过包装将数据源转化为ReadableStream如果想通过ReadableStream读取外部源,可以将其包装在适配器对象中并将该对象传递给构造函数ReadableStream。
interfaceUnderlyingSource{start(controller:ReadableStreamController):void|Promise;//忽略//pull(controller:ReadableStreamController):void|Promise;//cancel(reason:any):void|Promise;//type:'bytes'|undefined;//autoAllocateChunkSize:bigint;}controller的参数类型如下:
typeReadableStreamController=|ReadableStreamDefaultController|ReadableByteStreamController;//先忽略interfaceReadableStreamDefaultController{enqueue(chunk:TChunk):void;close():void;//忽略//readonlydesiredSize:number|Null;//error(err:any):void;}自定义ReadableStreamdemoasyncfunctiontest14(){constreadableStream=newReadableStream({start(controller){controller.enqueue('FirstLine\n');//(A)controller.enqueue('SecondLine\n');//(B)controller.close();//(C)},});forawait(constchunkofreadableStream){console.log(chunk);}}test14();//FirstLine//SecondLineReadableStream是异步可迭代的,可以使用for-await-of来进行迭代。
使用控制器创建一个包含两个块的流(A和B行),关闭流(C行)很重要,否则for-await-of永远不会结束。
转化流:
使用TransformStream最常见的方式是pipeThrough。
consttransformStream=readableStream.pipeThrough(transformStream);.pipeThrough()将readableStream传输到transformStream的可写端,并进行转换返回其可读端。
换句话说:创建了一个新的ReadableStream,它是ReadableStream的转换版本,类似于数组的map。
一个简单的demo:
asyncfunctiontest21(){constencoder=newTextEncoder();constreadableByteStream=newReadableStream({start(controller){controller.enqueue(encoder.encode('hello\n'));controller.enqueue(encoder.encode('world\n'));},});constreadableStream=readableByteStream.pipeThrough(newTextDecoderStream('utf-8'));forawait(conststringChunkofreadableStream){console.log(stringChunk);}}test21();TextEncoder.encode():将字符串作为输入,并返回Uint8Array包含UTF-8编码的文本。
使用了内置TransformStream:TextDecoderStream(),作用就是将接收到的二进制流转换为可读的文本流(Uint8Array->string)。
跟上面的ReadableStream类似,如果要自定义TransformStream,也可以传递适配器对象给构造函数TransformStream。
它具有以下类型:
interfaceTransformStream{start(controller:TransformStreamDefaultController):void|Promise;transform(chunk:InChunk,controller:TransformStreamDefaultController):void|Promise;//忽略//flush(//chunk:InChunk,//controller:TransformStreamDefaultController//):void|Promise;}上面属性的解释:
该contrller具有以下类型:
interfaceTransformStreamDefaultController{enqueue(chunk:OutChunk):void;terminate():void;//忽略//readonlydesiredSize:number|null;//error(err:any):void;}小demo说了一大堆api,来一个简单的例子:
asyncfunctiontest20(){//创建一个ReadableStream对象constreadableStream=newReadableStream({start(controller){controller.enqueue('hello');controller.enqueue('world');controller.close();},});//创建一个TransformStream对象consttransformer=newTransformStream({transform(chunk,controller){//对输入数据进行转换处理consttransformedChunk=chunk.toUpperCase();//将转换后的数据通过controller.enqueue()方法推送到输出流ReadableStreamcontroller.enqueue(transformedChunk);},});//通过TransformStream进行处理流的转换constnewReadableStream=readableStream.pipeThrough(transformer);//使用reader方式来读取ReadableStreamconstreader=newReadableStream.getReader();while(true){const{done,value}=awaitreader.read();if(done){break;}console.log(value);//HELLOWORLD}}test20();重新回顾fetchAPIFetchAPI是一种用于获取和发送网络资源的现代WebAPI。它提供了一种替代XMLHttpRequest的方式,可以更简单、更灵活地进行网络请求。
FetchAPI使用Promise对象来返回请求结果,可以轻松地将其与async/await结合使用。
简单的小例子:
需要注意的是:fetch.body返回的是二进制流,后面会再提到。
讲到这里,终于把前置的知识熟悉一下,我知道你很急,但是你先别急。
我们来进入实战环节。
首先使用pnpmcreatenext-app初始化一个Next13项目。
初始化之后就会安装TypeScript、Eslint和TailwindCSS。
第一步就开始画UI。
UI的话主要有两个部分:
创建类型文件type.ts定义关于message的类型。
//types.tsexporttypeMessage={id:string;createdAt:Date;content:string;role:'system'|'user'|'assistant';};MessageCard组件用来渲染输入和openai返回的信息。
import{Message}from'@/types';importclassNamesfrom'classnames';interfaceMessageCardProps{message:Message;}typeAvatarProps=Pick;constAvatar=({role}:AvatarProps)=>{constgetName=()=>(role==='user''U':'AI');return({getName()});};constMessageCard=({message}:MessageCardProps)=>{return({message.content}
);};exportdefaultMessageCard;基础页面+input输入框下一步画基础的页面和input输入框。直接在app/page.tsx里面书写即可。
//app/page.tsx'useclient';importMessageCardfrom'./MessageCard';import{Message}from'@/types';constChat=()=>{constmessages:Message[]=[{id:'1',content:'hello',role:'user'},{id:'2',content:'world',role:'assistant'},];return({messages.map((message)=>())}{e.preventDefault();}}>
);};exportdefaultChat;先mock消息列表,看看展示效果咋样。
在app目录下新建文件app/api/chat/route.ts,用来处理api请求。
需要发送POST请求将message传递给openai,通过exportfunctionPOST就可以处理POST请求。
//app/api/chat/route.tsexportconstruntime='edge';//如果要流式渲染则需要加上这一行exportasyncfunctionPOST(req:Request){conststream=AIStream();console.log('stream:',stream);returnnewStreamingTextResponse(stream);}libs/streaming-text-response.ts就是对于Response的简单封装,将状态码置为200。
//libs/streaming-text-response.tsexportclassStreamingTextResponseextendsResponse{constructor(res:ReadableStream,init:ResponseInit){super(resasany,{...init,status:200,headers:{'Content-Type':'text/plain;charset=utf-8',...init.headers,},});}}创建文件libs/ai-stream.ts用来处理网络流,使用ReadableStream先mock两条数据。
//libs/ai-stream.tsexportfunctionAIStream():ReadableStream{conststream=newReadableStream({start(controller){controller.enqueue('hello\n');controller.enqueue('world\n');controller.close();},});returnstream;}hook:use-chat下一步写一个hook来进行页面交互,创建文件hooks/use-chat.ts。
需要安装一些依赖包:nanoid和swr。
新建utils.ts文件用来保存两个工具函数:nanoid、createChunkDecoder。
//utils.tsimport{customAlphabet}from'nanoid';exportconstnanoid=customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',7);exportfunctioncreateChunkDecoder(){constdecoder=newTextDecoder();returnfunction(chunk:Uint8Array|undefined):string{if(!chunk)return'';returndecoder.decode(chunk,{stream:true});};}请求接口接下来就是发送网络请求到/api/chat,请求成功之后将数据渲染出来。
//app/page.tsxconstChat=()=>{const{messages,input,handleSubmit,handleInputChange}=useChat();return({messages.map((message)=>())}
);};exportdefaultChat;连通性验证不出意外的报bug了。问题在于fetch.body方法返回的是一个二进制流(Uint8Array),fetch提供了text()、json()、blob()等方式将二进制流转化为其他数据格式。
刚刚在libs/ai-stream.ts里面推到队列里面的是字符串,所以就会报错。
exportfunctionAIStream():ReadableStream{conststream=newReadableStream({start(controller){controller.enqueue('hello\n');//字符串controller.enqueue('world\n');//字符串controller.close();},});returnstream;}有两种解决方式。
consttextEncoder=newTextEncoder();conststream=newReadableStream({start(controller){controller.enqueue(textEncoder.encode('hello\n'));controller.enqueue(textEncoder.encode('world\n'));controller.close();},});创建了TextEncoder对象,用于将字符串编码为Uint8Array对象。
exportfunctioncreateCallbacksTransformer(){constencoder=newTextEncoder();returnnewTransformStream({asynctransform(message,controller):Promise{controller.enqueue(encoder.encode(message));},});}exportfunctionAIStream():ReadableStream{conststream=newReadableStream({start(controller){controller.enqueue('hello\n');controller.enqueue('world\n');controller.close();},});returnstream.pipeThrough(createCallbacksTransformer());}通过使用pipeThrough方法,将AIStream的输出流连接到createCallbacksTransformer的输入流,实现了数据的转换和传递。
实现将string转换为Uint8Array对象的流处理过程。
本文后面会使用第二种方式。
我们来看看效果:
完成了一大步。
在连接openai之前,需要先安装openai-edge。
pnpmaddopenai-edge接下来就是在app/api/chat/route.ts中连接openai。
//app/api/chat/route.tsconstconfig=newConfiguration({//申请好的OPENAI_API_KEYapiKey:process.env.OPENAI_API_KEY,});constopenai=newOpenAIApi(config);exportconstruntime='edge';exportasyncfunctionPOST(req:Request){//获得请求参数const{messages}=awaitreq.json();constresponse=awaitopenai.createChatCompletion({model:'gpt-3.5-turbo',stream:true,messages:messages.map((message:any)=>({content:message.content,role:message.role,})),});//将response传递给AIStream进行处理conststream=AIStream(response);returnnewStreamingTextResponse(stream);}这里process.env.OPENAI_API_KEY读取的是环境变量,我们可以新建一个.env.local的文件。
OPENAI_API_KEY=apikey新建完成之后需要重启一下服务,加载一下环境变量。
使用.env.local命名是为了防止将该文件提交到git,如果上传到github会暴露apiKey。
在服务端请求openaiapi,并将openai返回的response传递给了AIStream,AIStream也需要更新一下。
//libs/ai-stream.tsexportfunctionAIStream(res:Response):ReadableStream{if(!res.ok){thrownewError(`Failedtoconverttheresponsetostream.Receivedstatuscode:${res.status}.`);}conststream=res.body||newReadableStream({start(controller){controller.close();},});returnstream;//.pipeThrough(createCallbacksTransformer);}res.body返回的是ReadableStream,之前写的pipeThrough(createCallbacksTransformer)是将string->Uint8Array,目前先用不到,先注释掉。
再来看看效果:
从gif图上看到返回"data:json字符串"这样子的格式,这是sse数据格式。
sse数据:每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。
每个message内部由若干行组成,每一行都是如下格式。
[field]:value\n上面的field可以取四个值。
sse数据如何进行解析呢?可以使用eventsource-parser这个库,这个库就是用来解析sse数据的。
pnpmaddeventsource-parserfetch.body返回的是Uint8Array,还是使用TransformStream,在TransformStream中进行sse数据的解析。
先新建函数createEventStreamTransformer:
//libs/ai-stream.tsimport{createParser,typeEventSourceParser,typeParseEvent,}from'eventsource-parser';exportfunctioncreateEventStreamTransformer(customParser:AIStreamParser){constdecoder=newTextDecoder();letparser:EventSourceParser;returnnewTransformStream({asyncstart(controller){functiononParse(event:ParseEvent){if(event.type==='event'){constdata=event.data;//data就是解析之后的每个数据if(data==='[DONE]'){//如果是[DONE]表示解析完毕。controller.terminate();return;}//将json数据字符串传入到自定义解析器进行过滤constmessage=customParser(data);//如果message有值的话,就推到队列中。if(message)controller.enqueue(message);}}//创建解析器parser=createParser(onParse);},transform(chunk){parser.feed(decoder.decode(chunk));},});}在start方法中,创建了parser,用来解析数据流中的sse数据。
在transform方法中,此时chunk是Uint8Array类型,先使用TextDecoder将Uint8Array解析成字符串,然后通过feed进行sse数据的解析,解析后的数据传递给onParse函数。
这里为了逻辑解耦,使用了customParser自定义解析器,传入customParser来实现解析具体的逻辑(下面会介绍)。
流可以使用多次pipeThrough,进行多次转化。
createEventStreamTransformer方法传入了自定义解析器,customParser该怎么写呢?
libs/ai-stream.ts文件就只负责流的处理,将customParser作为参数传入即可,具体如何解析取决于上层应用。
//libs/ai-stream.tsexportfunctionAIStream(res:Response,customParser:AIStreamParser//新增):ReadableStream{//省略其他代码...returnstream.pipeThrough(createEventStreamTransformer(customParser))//传入.pipeThrough(createCallbacksTransformer());}新建文件libs/openai-stream.ts实现openai流的customParser。
//libs/openai-stream.tsimport{AIStream}from'./ai-stream';exportfunctiontrimStartOfStreamHelper(){letstart=true;return(text:string)=>{if(start)text=text.trimStart();if(text)start=false;returntext;};}functionparseOpenAIStream():(data:string)=>string|void{consttrimStartOfStream=trimStartOfStreamHelper();return(data)=>{constjson=JSON.parse(data);//将json字符串解析成对象consttext=trimStartOfStream(json.choices[0].delta.contentjson.choices[0].text'');//读取对应的字段returntext;};}exportfunctionOpenAIStream(res:Response):ReadableStream{returnAIStream(res,parseOpenAIStream());}一个消息头部可能有多个空格,使用trimStartOfStreamHelper辅助函数把chunk最前面的空格给去掉。
因为data传入的时候已经是sse的数据部分,是json字符串,可以使用JSON.parse的方式来解析成对象,最后在读取相对应的字段即可。
再更新一下app/api/chat/route.ts文件,将AIStream替换成OpenAIStream。
//app/api/chat/route.tsexportasyncfunctionPOST(req:Request){//省略其他代码...conststream=OpenAIStream(response);//AIStream()->OpenAIStream()returnnewStreamingTextResponse(stream);}再来看看效果
牛哇,实现啦!!!
如果对当前生成的结果不满意,重新生成新的结果。
//hooks/use-chat.tsexporttypeUseChatHelpers={reload:()=>Promise;};exportfunctionuseChat({api='/api/chat',id,initialInput='',initialMessages=[],}:UseChatOptions={}):UseChatHelpers{//省略其他代码...constreload=useCallback(async()=>{if(messagesRef.current.length===0)returnnull;constlastMessage=messagesRef.current[messagesRef.current.length-1];//如果最后一条消息是chatgpt生成的if(lastMessage.role==='assistant'){//去掉消息列表的最后一条消息,然后触发接口请求returntrigger(messagesRef.current.slice(0,-1));}returntrigger(messagesRef.current);},[trigger]);return{//省略其他的代码...reload,};}新增react-feather添加几个好看的图标,顺便也把input输入框美化一下。
在hooks/use-chat.ts中使用了AbortController传递给了fetch函数,写一个stop方法来实现暂停。
//hooks/use-chat.tsexporttypeUseChatHelpers={stop:()=>void;};exportfunctionuseChat({api='/api/chat',id,initialInput='',initialMessages=[],}:UseChatOptions={}):UseChatHelpers{//省略其他代码...conststop=useCallback(()=>{if(abortControllerRef.current){//取消请求abortControllerRef.current.abort();abortControllerRef.current=null;}},[]);return{//省略其他的代码...stop,};}如果在生成中会出现暂停的图标,再更新一下UI。
//app/page.tsximport{Pause,Send}from'react-feather';constChat=()=>{//省略其它代码...const{stop,isLoading,messages}=useChat();constgetBtnContent=()=>{if(isLoading){return(<>暂停生成>);}return(<>重新生成>);};return(//省略其他代码...{messages.length>0({getBtnContent()}):null}{/*省略其他代码...*/});};来看看效果
有一点小缺陷:点击stop只是前端不再读取网络流,所以只是前端的渲染暂停,但是此时网络流还是没有断,更好的方式应该是发送消息给后端,后端主动中断流,然后再停止前端的渲染。
openai是支持写markdown格式的,可以引入markdown->html的包进行渲染。
需要安装marked和@tailwindcss/typography。
pnpmaddmarkedpnpmadd@tailwindcss/typography@types/marked-D新建libs/marked.ts文件,用来写markdown->HTML的方法
//libs/marked.tsimport{marked}from'marked';exportconstmarkdownToHTML=(markdown:string)=>{if(!markdown||typeofmarkdown!=='string'){return'';}returnmarked.parse(markdown);};@tailwindcss/typography的配置也比较简单,在tailwind.config.js的plugin配置一下。
//tailwind.config.js/**@type{import('tailwindcss').Config}*/module.exports={//省略其他代码...plugins:[require('@tailwindcss/typography')],};在app/MessageCard.tsx文件中,给需要进行渲染markdownhtml的内容添加上类名:prose。
//app/MessageCard.tsximport{markdownToHTML}from'@/libs/marked';constMessageCard=({message}:MessageCardProps)=>{constcontent=markdownToHTML(message.content);return();};来看看效果:
如果渲染的消息过长,或者消息过多时,得手动进行滚动,写一个hook要滚动条一直维持在底部。
新建hook:hooks/use-scroll-bottom。
需要先安装lodash.throttle
pnpmaddlodash.throttle//hooks/use-scroll-bottom.tsimportthrottlefrom'lodash.throttle';import{RefObject,useEffect}from'react';interfaceUseScrollBottomOptions{scrollRef:RefObject;}constuseScrollBottom=({scrollRef}:UseScrollBottomOptions)=>{useEffect(()=>{constscrollingElement=scrollRef.current;constcallback:MutationCallback=function(mutationsList){for(letmutationofmutationsList){if(mutation.type==='childList'){window.scrollTo(0,document.body.scrollHeight);}}};constthrottleCallback=throttle(callback,1000/16);constobserver=newMutationObserver(throttleCallback);if(scrollingElement){observer.observe(scrollingElement!,{subtree:true,childList:true,});}return()=>{observer.disconnect();};},[]);};exportdefaultuseScrollBottom;在app/page.tsx中导入进行使用即可。
本来只想写流式渲染的,发现写着写着可以写成一个乞丐版的chatgpt,然后就一路写写写写下去了。
THE END
1.chatgpt无法打开页面3. 使用稳定的网络连接:选择稳定的网络连接,避免在网络不稳定或连接质量较差的情况下访问ChatGPT页面。 4. 提供反馈:如果您遇到ChatGPT无法打开页面的问题,建议您向OpenAI团队提供反馈,以帮助他们改进和优化用户体验。 “ChatGPT无法打开页面”可能是由于多种原因造成的,但我们可以采取一些解决方法来解决这个问题。通过http://chatgpt.cmpy.cn/article/1660221.html
2.怎样用chatgpt快速写一个具有动效的页面chatgpt页面文章标签: chatgpt css 前端 版权 文章描述了一个使用HTML、CSS和JavaScript构建的交互式页面,页面根据URL参数改变内容,并通过点击事件监听器切换输入框的默认值。当用户点击按钮时,使用axios库将数据以POST方式提交到后端。 摘要由CSDN通过智能技术生成 下图是想做的页面效果,根据请求url的不同,图标以及下面的提示语https://blog.csdn.net/qq_17858343/article/details/130314190
3.Edge上的ChatGPT新标签页如果你认为此加载项违反了Microsoft Store 内容策略,请使用此表单。 提供电子邮件地址 包括你的电子邮件地址,即表示你同意 Microsoft 可以就你的反馈向你发送电子邮件。Microsoft 隐私声明 输入你看到的字符。你也可以选择音频质询。 新|视觉 提交https://microsoftedge.microsoft.com/addons/report/bjfdgkgifejgainjdpdhdjdciglicggd
4.如何登录网页版CHATGPT3. 当网页加载完成后,您将看到CHATGPT的欢迎页面。4. 在欢迎页面中,您可以选择“Sign in”(登录)http://chatgpt.kuyin.cn/ask/1817097.html
5.GitHub抓包发现ChatGPT页面都会上传大量的用户环境信息,进行大量的行为分析、用户跟踪。 勾选了拦截跟踪以后,可以拦截大部分的跟踪行为,保护了用户的信息安全,提高页面加载速度。 经测试,刷新ChatGPT页面会产生50~100个网络请求,其中至少15~65个网络请求是在跟踪、分析用户! https://github.com/xcanwin/KeepChatGPT
6.搭建一个chatgpt网页聊天界面然后还需要准备一个chatgpt账号,有api也行。首先如果有账号的话就去chatgpt管理页面获取个api,进到首页,点击右上角头像,下拉菜单中点击“View API keys”,进去新建个api即可,这个过程需要上网环境的支持,所以还是花点小钱直接买比较方便。 准备好api后,我们去github上找到“Chanzhaoyu/chatgpt-web”这个项目,可以https://www.jianshu.com/p/bb6118c2469a
7.用Python搭建一个ChatGPT聊天页面如何使用python搭建一个chatgpt聊天页面呢?今天我们一起来了解一下。 搭建一个基于Python的ChatGPT聊天页面通常涉及以下几个步骤: 创建Web应用框架 创建HTML聊天界面 实现后端逻辑 完善前端JavaScript 创建Web应用框架:使用Python的Web开发框架,如Flask或Django,来构建基础的Web应用程序。这里以Flask为例,首先安装Flask: https://www.51cto.com/article/785401.html
8.ChatGPT网页版的使用方法目前中国用户无法通过ChatGPT官网注册账号。如果没有ChatGPT的注册账号,怎么使用ChatGPT网页版聊天呢?OpenAI授权一些机构或个人使用ChatGPT的接口程序,其中一些用户会将自己的接口界面共享给其它用户使用,在接口网页版输入问题,经ChatGPT后端生成回复内容后再返回到接口页面,从而间接地实现与ChatGPT提问和对话。 http://www.wwiki.cn/wiki/215399.htm
9.ChatGPT官网版免费使用指南:网页版轻松操作–ChatGPT中文网页版www.chatgp4.com 在人工智能领域,ChatGPT作为一款备受瞩目的聊天机器人,以其强大的语言理解和生成能力吸引了众多用户的关注。今天,我们将为您详细介绍如何免费使用ChatGPT官网版的网页端,让您轻松体验这一前沿技术的便捷与魅力。 一、注册与登录 首先,访问ChatGPT官网(https://www.chatgp4.com/),点击页面右上角的https://chat.729.cn/?p=2221
10.ChatGPT登录后无法操作怎么解决ip资讯尝试使用其他浏览器:有时浏览器本身可能存在兼容性问题。尝试使用不同的浏览器(如Google Chrome、Mozilla Firefox或Microsoft Edge)访问ChatGPT,看是否可以解决问题。 禁用浏览器插件或扩展:某些浏览器插件或扩展可能会干扰ChatGPT的正常功能。尝试禁用所有插件或扩展,然后重新加载ChatGPT页面。 https://www.kookeey.com/news/archives/10873
11.火爆全网的ChatGPT小程序页面模板,让AI回答你的任何问题!ChatGPT 是OpenAI 开发的一款专门从事对话的人工智能聊天机器人原型。聊天机器人是一种大型语言模型,采用监督学习和强化学习技术。ChatGPT 于2022 年 11 月推出,尽管其回答事实的准确性受到批评,但因其详细和清晰的回复而受到关注。ChatGPT 使用监督学习和强化学习在 GPT-3.5 之上进行了微调和升级。ChatGPT的相关模型https://www.dkewl.com/code/detail2562.html
12.chatgtp中国免费网页版常用的方法:chatgpt如何使用?chatgtp中国免费网页版 常用的方法:chatgpt如何使用? 朋友你好,在此相见,必定有缘!我是只讲干货的刘老师。15年网商实战,8年培训经验,擅长暴力获取免费流量!推荐好文,分享干货,用心教课。希望可以助您一臂之力。 重点推荐本文,我个人觉得相当不错,多读几遍,对你会有帮助。【猛点这里>>>更多干货】http://www.mkcmgs.cn/sys-nd/6871.html
13.在AppStore上的「ChatGPT」Introducing ChatGPT for iOS: OpenAI’s latest advancements at your fingertips. This official app is free, syncs your history across devices, and brings you the…https://apps.apple.com/tw/app/chatgpt/id6448311069
14.ChatGPT怎么改密码?Worktile社区更改ChatGPT的密码是保护您的账户和数据安全的重要步骤。下面是一步一步指导您如何修改ChatGPT的密码: 步骤一:登录到ChatGPT帐户1. 打开 ChatGPT 登录页面。2. 输入您的用户名和当前密码。3. 单击“登录”。 步骤二:访问帐户设置1. 一旦成功登录,您将进入ChatGPT的主页。2. 在页面右上角,您将看到一个下拉菜https://worktile.com/kb/ask/538282.html
15.ChatGPT从哪里进入ChatGPT入口地址介绍其他工具软件教程ChatGPT人工智能ai机器人和自己对话的感受肯定非常不错。但是有些朋友目前还不知道从哪里进入chatgpt的页面进行聊天,接下来小编会为大家带来详细的教程。 墨客诗词(古诗词学习软件) v2.0.5 安卓手机版 类型:学习教育 大小:53.9MB 语言:简体中文 时间:2023-12-12 https://m.jb51.net/softjc/867422.html
16.ChatGPT网页端预处理配合浏览器实现长图下载吾爱破解有一个师姐问我ChatGPT怎么截长图。一开始想很简单吧,谷歌浏览器和Edge浏览器的开发者工具自带截长图https://www.52pojie.cn/thread-1790136-1-1.html