├── README.md ├── diagram.png ├── diagram2.jpg ├── facturas.csv ├── main.py ├── main_agentia.py ├── main_rag.py ├── requirements.txt ├── static ├── css │ └── app.css ├── img │ └── bg3.jpg ├── index.html └── js │ └── app.js └── testchromadb.py /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Tabla de contenidos 3 | 4 | - [Descripción del proyecto](#descripción-del-proyecto) 5 | - [Pasos para la puesta en marcha](#pasos-para-la-puesta-en-marcha) 6 | - [Arrancar LLM(Large Language Model)](#arrancar-llm(large-language-model)) 7 | - [Instalación de dependencias Python](#instalación-de-dependencias-python) 8 | - [Puesta en marcha del chatbot básico](#puesta-en-marcha-del-chatbot-básico) 9 | - [Puesta en marcha del chatbot con RAG](#puesta-en-marcha-del-chatbot-con-rag) 10 | - [Probar el script para trastear con ChromaDB](#probar-el-script-para-trastear-con-chromadb) 11 | - [Puesta en marcha del agente de IA](#puesta-en-marcha-del-agente-de-ia) 12 | - [Técnicas con LLMs](#técnicas-con-llms) 13 | - [LLMs confrontados](#llms-confrontados) 14 | - [Arquitectura de "Verificación por consenso"](#arquitectura-de-"verificación-por-consenso") 15 | - [Arquitectura de "Detección de anomalías"](#arquitectura-de-"detección-de-anomalías") 16 | - [Arquitectura de "Generación y edición iterativa"](#arquitectura-de-"generación-y-edición-iterativa") 17 | - [Referencias](#referencias) 18 | 19 | 20 | # Descripción del proyecto 21 | 22 | Este proyecto busca convertirse en un punto de referencia accesible para cualquier persona interesada en el fascinante mundo de los chatbots y los modelos de lenguaje extenso (LLMs de sus siglas en inglés Large Language Model). Nuestro objetivo es proporcionar una base sólida de conocimientos y herramientas prácticas que permitan a usuarios de todos los niveles, desde principiantes hasta desarrolladores experimentados, adentrarse en la creación de sus propios asistentes y aplicaciones con LLMs. Queremos democratizar el acceso a esta tecnología y fomentar la innovación en el campo de la interacción humano-máquina. 23 | 24 | De momento tenemos implementados y explicados tres técnicas básicas para crear chatbots y aplicaciones con LLMs: Chatbot básico(main.py), RAG(main_rag.py) y un agente de IA(main_agentia.py): 25 | 26 |  27 | 28 | # Pasos para la puesta en marcha 29 | 30 | ## Arrancar LLM(Large Language Model) 31 | 32 | Para empezar, procedemos a inicializar el modelo de lenguaje LLM(Large Language Model) que servirá como base para nuestro chatbot. Para facilitar su implementación y gestión, decidimos ejecutar los LLMs a través de un contenedor Docker utilizando la plataforma Cortex. Esta elección nos permite aislar el entorno de ejecución del LLM, garantizando una mayor estabilidad y portabilidad del proyecto y ejecutarlo usando la GPU o CPU. 33 | 34 | ``` 35 | # Arrancamos el contenendor usando la CPU 36 | docker run -it -d --name cortex -p 39281:39281 menloltd/cortex 37 | 38 | # O alternativamente, usando la GPU. Atención! Requiere los drivers 'nvidia-docker' y evidentemente el hardware adecuado 39 | docker run --gpus all -it -d --name cortex -p 39281:39281 menloltd/cortex 40 | ``` 41 | 42 | En este caso, hemos seleccionado Llama 3.2 3B, un modelo de última generación conocido por su capacidad para generar texto de alta calidad aunque podemos usar otros modelos: 43 | 44 | ``` 45 | # Descargamos el modelo y lo arrancamos 46 | docker exec -it cortex cortex run llama3.2:3b-gguf-q4-km 47 | 48 | # O alternativamente, podemos usar otros modelos, como Phi de Microsoft 49 | docker exec -it cortex cortex run phi-3.5:3b-gguf-q4-km 50 | 51 | # O alternativamente, podemos usar otros modelos, como DeepSeek 52 | docker exec -it cortex cortex run deepseek-r1-distill-qwen-14b:14b-gguf-q4-km 53 | ``` 54 | 55 | Atención! Es importante tener configurado el modelo que tenemos descargado y arrancado en los scripts "main_*.py" 56 | 57 | ## Instalación de dependencias Python 58 | 59 | Para arrancar el chatbot el primer paso obligatório es instalar las dependencias del python(En nuestro caso vamos a crear también un entorno virtual de Python con el objetivo de tener un único espacio con las dependencias de nuestro proyecto): 60 | 61 | ``` 62 | # Creación y activación del virtual env 63 | virtualenv env 64 | source env/bin/activate 65 | 66 | # Instalaciión de dependencias 67 | pip install -r requirements.txt 68 | ``` 69 | 70 | ## Puesta en marcha del chatbot básico 71 | 72 | Para arrancar el chatbot simplemente arrancar el script 'main.py': 73 | 74 | ``` 75 | # Arranque del script 76 | python main.py 77 | ``` 78 | 79 | ## Puesta en marcha del chatbot con RAG 80 | 81 | Un RAG (Retrieval Augmented Generation, o Generación Aumentada por Recuperación) es una técnica de inteligencia artificial que combina la capacidad de los LLMs y la habilidad de buscar información específica en una base de datos(Normalmente una base de datos vectorial). 82 | 83 | Para arrancar el chatbot con el RAG simplemente arrancar el script 'main_rag.py': 84 | 85 | ``` 86 | # Arranque del script 87 | python main_rag.py 88 | ``` 89 | 90 | ### Probar el script para trastear con ChromaDB 91 | 92 | Para arrancar el script y trastear con ChromaDB(Base de datos orientada a vectores) ejecutamos el script 'testchromadb.py': 93 | 94 | ``` 95 | # Arranque del script 96 | python testchromadb.py 97 | ``` 98 | 99 | ## Puesta en marcha del agente de IA 100 | 101 | Un agente de IA es un sistema que percibe su entorno, toma decisiones y realiza acciones con el objetivo de alcanzar metas específicas. Para ello se conecta a distintos tipos de herramientas como bases de datos, APIs, dispositivos, etc. 102 | 103 | Para arrancar el chatbot con el Agente de IA simplemente arrancar el script 'main_agentia.py': 104 | 105 | ``` 106 | # Arranque del script 107 | python main_agentia.py 108 | ``` 109 | # Técnicas con LLMs 110 | 111 | ## LLMs confrontados 112 | 113 | La idea es confrontar la salida de un LLM con otro LLM y tiene el potencial de mejorar significativamente la calidad y confiabilidad de la información generada. Aquí presentamos algunas arquitecturas posibles y sus implicaciones: 114 | 115 |  116 | 117 | ### Arquitectura de "Verificación por consenso" 118 | 119 | **Funcionamiento:** Se utilizan múltiples LLMs para generar respuestas a una misma pregunta o tarea(En paralelo). Luego, se comparan las salidas y se selecciona la respuesta que tenga mayor consenso. 120 | 121 | **Ventajas:** Reduce la probabilidad de respuestas incorrectas o sesgadas, ya que se basa en la validación cruzada entre diferentes modelos. El ejecutarse el paralelo no penaliza la UX del usuario. 122 | 123 | **Desventajas:** Aumenta la complejidad y el costo computacional, ya que requiere la ejecución de múltiples LLMs. 124 | 125 | ### Arquitectura de "Detección de anomalías" 126 | 127 | **Funcionamiento:** Un LLM genera una respuesta, y luego otro LLM actúa como un "detector de anomalías" que evalúa la respuesta en busca de inconsistencias, errores o información falsa. 128 | 129 | **Ventajas:** Permite identificar y corregir errores en las respuestas generadas por el primer LLM, mejorando la precisión y confiabilidad de la información. 130 | 131 | **Desventajas:** Requiere un diseño cuidadoso del "detector de anomalías" para evitar falsos positivos o negativos. 132 | 133 | ### Arquitectura de "Generación y edición iterativa" 134 | 135 | **Funcionamiento:** Un LLM genera una primera versión de una respuesta, y luego otro LLM la revisa y edita para mejorar su calidad, claridad o precisión. Este proceso puede repetirse varias veces de forma iterativa. 136 | 137 | **Ventajas:** Permite obtener respuestas más elaboradas y pulidas, ya que se basa en la colaboración entre dos o mas LLMs. 138 | 139 | **Desventajas:** Puede ser un proceso lento y costoso, ya que requiere múltiples iteraciones en série. 140 | 141 | # Referencias 142 | 143 | - Vídeo de Youtube ["Aprende a desarrollar chatbots desde 0"](https://www.youtube.com/watch?v=Q_NLkUsJJ2c) 144 | - Vídeo de Youtube ["Aprende a desarrollar chatbots con RAG(Retrieval-Augmented Generation) usando ChromaDB"](https://www.youtube.com/watch?v=Etx2WSKQUS0) 145 | - Vídeo de Youtube ["Tutorial de desarrollo de IA Agents"](https://www.youtube.com/watch?v=40rc2-QBQ5g) 146 | - Vídeo de Youtube ["Aprende a crear aplicaciones con DeepSeek y Técnicas con LLMs confrontadas"](https://www.youtube.com/watch?v=ACGXJ13cvEQ) 147 | - Artículo ["Cortex: Desplegando LLMs en local"](https://www.albertcoronado.com/2024/12/03/cortex-plataforma-de-ia-para-desplegar-llms-en-local) 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoronadoc/chatbot-sample/b496680c849d0fab38a5c91d77b6a7f8e3325171/diagram.png -------------------------------------------------------------------------------- /diagram2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoronadoc/chatbot-sample/b496680c849d0fab38a5c91d77b6a7f8e3325171/diagram2.jpg -------------------------------------------------------------------------------- /facturas.csv: -------------------------------------------------------------------------------- 1 | fecha,cliente,pais,importe 2 | 2024-01-01,01,ES,15 3 | 2024-01-12,02,ES,15 4 | 2024-01-23,03,ES,15 5 | 2024-02-01,01,UK,15 6 | 2024-02-01,02,ES,15 7 | 2024-02-11,01,ES,15 8 | 2024-03-01,01,UK,15 9 | 2024-03-01,02,ES,15 10 | 2024-03-01,03,ES,15 11 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect 2 | from fastapi.responses import HTMLResponse, RedirectResponse 3 | from fastapi.staticfiles import StaticFiles 4 | from openai import AsyncOpenAI 5 | from websockets.exceptions import ConnectionClosed 6 | 7 | import uvicorn 8 | 9 | ENDPOINT = "http://127.0.0.1:39281/v1" 10 | #MODEL = "phi-3.5:3b-gguf-q4-km" 11 | #MODEL = "deepseek-r1-distill-qwen-14b:14b-gguf-q4-km" 12 | MODEL = "llama3.2:3b-gguf-q4-km" 13 | 14 | client = AsyncOpenAI( 15 | base_url=ENDPOINT, 16 | api_key="not-needed" 17 | ) 18 | 19 | app = FastAPI() 20 | 21 | app.mount("/static", StaticFiles(directory="static"), name="static") 22 | 23 | @app.get("/", response_class=HTMLResponse) 24 | async def root( request: Request ): 25 | return RedirectResponse("/static/index.html") 26 | 27 | @app.websocket("/init") 28 | async def init( websocket: WebSocket ): 29 | await websocket.accept() 30 | 31 | try: 32 | while True: 33 | data = await websocket.receive_json() 34 | 35 | await websocket.send_json( { "action": "init_system_response" } ) 36 | response = await process_messages( data, websocket ) 37 | await websocket.send_json( { "action": "finish_system_response" } ) 38 | except (WebSocketDisconnect, ConnectionClosed): 39 | print( "Conexión cerrada" ) 40 | 41 | async def process_messages( messages, websocket ): 42 | completion_payload = { 43 | "messages": messages 44 | } 45 | 46 | response = await client.chat.completions.create( 47 | top_p=0.9, 48 | temperature=0.6, 49 | model=MODEL, 50 | messages=completion_payload["messages"], 51 | stream=True 52 | ) 53 | 54 | respStr = "" 55 | async for chunk in response: 56 | if (not chunk.choices[0] or 57 | not chunk.choices[0].delta or 58 | not chunk.choices[0].delta.content): 59 | continue 60 | 61 | await websocket.send_json( { "action": "append_system_response", "content": chunk.choices[0].delta.content } ) 62 | 63 | return respStr 64 | 65 | 66 | uvicorn.run(app, host="0.0.0.0", port=8000) 67 | 68 | -------------------------------------------------------------------------------- /main_agentia.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect 2 | from fastapi.responses import HTMLResponse, RedirectResponse 3 | from fastapi.staticfiles import StaticFiles 4 | from openai import AsyncOpenAI, OpenAI 5 | from websockets.exceptions import ConnectionClosed 6 | 7 | import duckdb 8 | import uvicorn 9 | 10 | ENDPOINT = "http://127.0.0.1:39281/v1" 11 | #MODEL = "phi-3.5:3b-gguf-q4-km" 12 | #MODEL = "llama3.2:3b-gguf-q4-km" 13 | MODEL = "deepseek-r1-distill-qwen-14b:14b-gguf-q4-km" 14 | 15 | client = AsyncOpenAI( 16 | base_url=ENDPOINT, 17 | api_key="not-needed" 18 | ) 19 | 20 | client2 = OpenAI( 21 | base_url=ENDPOINT, 22 | api_key="not-needed" 23 | ) 24 | 25 | app = FastAPI() 26 | 27 | app.mount("/static", StaticFiles(directory="static"), name="static") 28 | 29 | @app.get("/", response_class=HTMLResponse) 30 | async def root( request: Request ): 31 | return RedirectResponse("/static/index.html") 32 | 33 | @app.websocket("/init") 34 | async def init( websocket: WebSocket ): 35 | await websocket.accept() 36 | 37 | try: 38 | while True: 39 | data = await websocket.receive_json() 40 | 41 | await websocket.send_json( { "action": "init_system_response" } ) 42 | response = await plan_messages( data, websocket ) 43 | await websocket.send_json( { "action": "finish_system_response" } ) 44 | except (WebSocketDisconnect, ConnectionClosed): 45 | print( "Conexión cerrada" ) 46 | 47 | async def plan_messages( messages, websocket ): 48 | pmsg = [ 49 | { "role": "system", "content": """ 50 | Responde solo 'No' en caso de no ser posible responder con los datos de la tabla. 51 | Responde solo 'No' en caso de pedir información de empresas que no sean Lostsys. 52 | Responde solo la sentencia SQL para la base de datos DuckDB a ejecutar en caso de poder obtener los datos de la tabla. 53 | Responde siempre Sin explicación, Sin notas, Sin delimitaciones. 54 | Disponemos de una tabla de base de datos llamada 'facturas' que contiene estos campos: 'fecha' del tipo VARCHAR usando el formato de fecha 'YYYY-MM-DD', 'cliente' tipo INTEGER que contiene el id de cliente, 'pais' tipo VARCHAR que contiene 'ES' como España y 'UK' como reino unido, 'importe' con el total de la factura. 55 | No puedes suponer nada ni usar otras tablas o datos que no sean los de la tabla 'facturas'. 56 | Para filtrar por el campo fecha usa siempre 'LIKE', nunca utilices funciones de fecha como YEAR, MONTH, EXTRACT. 57 | """ }, 58 | { "role": "user", "content": messages[ -1 ]["content"] } 59 | ] 60 | 61 | response = client2.chat.completions.create( 62 | top_p=0.9, 63 | temperature=0.9, 64 | model=MODEL, 65 | messages=pmsg, 66 | ) 67 | 68 | r = response.choices[0].message.content 69 | print( r ) 70 | r = clean_sql( r ) 71 | 72 | if not r.startswith("No"): 73 | await websocket.send_json( { "action": "append_system_response", "content": r } ) 74 | await websocket.send_json( { "action": "append_system_response", "content": "\n\nResultado: " + execute__query( r ) } ) 75 | 76 | return 77 | 78 | return await process_messages( messages, websocket ) 79 | 80 | def execute__query( sql ): 81 | return str( duckdb.sql( sql ).fetchall() ) 82 | 83 | def clean_sql( sql ): 84 | if sql.find("<|end_of_text|>") != -1: sql = sql[sql.find("<|end_of_text|>")+15:] 85 | if sql.find("") != -1: sql = sql[sql.find("")+8:] 86 | 87 | sql = sql.strip() 88 | 89 | if sql.startswith("```sql"): sql = sql[6:] 90 | if sql.startswith("```"): sql = sql[3:] 91 | if sql.endswith("```"): sql = sql[:len(sql)-3] 92 | if sql.find("```") != -1: sql = sql[sql.find("```"):] 93 | sql = sql.replace( "FROM facturas", "FROM './facturas.csv'" ) 94 | sql = sql.replace( "fecha", "CAST(fecha AS VARCHAR)" ) 95 | 96 | return sql 97 | 98 | 99 | async def process_messages( messages, websocket ): 100 | completion_payload = { 101 | "messages": messages 102 | } 103 | 104 | response = await client.chat.completions.create( 105 | top_p=0.9, 106 | temperature=0.6, 107 | model=MODEL, 108 | messages=completion_payload["messages"], 109 | stream=True 110 | ) 111 | 112 | respStr = "" 113 | async for chunk in response: 114 | if (not chunk.choices[0] or 115 | not chunk.choices[0].delta or 116 | not chunk.choices[0].delta.content): 117 | continue 118 | 119 | await websocket.send_json( { "action": "append_system_response", "content": chunk.choices[0].delta.content } ) 120 | 121 | return respStr 122 | 123 | 124 | uvicorn.run(app, host="0.0.0.0", port=8000) 125 | 126 | -------------------------------------------------------------------------------- /main_rag.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect 2 | from fastapi.responses import HTMLResponse, RedirectResponse 3 | from fastapi.staticfiles import StaticFiles 4 | from openai import AsyncOpenAI 5 | from websockets.exceptions import ConnectionClosed 6 | 7 | import chromadb 8 | import json 9 | import uvicorn 10 | 11 | ENDPOINT = "http://127.0.0.1:39281/v1" 12 | #MODEL = "phi-3.5:3b-gguf-q4-km" 13 | #MODEL = "deepseek-r1-distill-qwen-14b:14b-gguf-q4-km" 14 | MODEL = "llama3.2:3b-gguf-q4-km" 15 | 16 | client = chromadb.Client() 17 | 18 | collection = client.create_collection("all-my-documents") 19 | 20 | collection.add( 21 | documents=[ 22 | "La empresa Lostsys se dedica a ofrecer servícios y productos a empresas sobre informática corporativa como software de gestión, CRMs, ERPs, portales corporativos, eCommerce, formación, DevOps, etc.", 23 | "En Lostsys podemos ayudarte ha mejorar tus procesos de CI/CD con nuestros productos y servícios de DevOps.", 24 | "En Lostsys podemos ayudarte a digitalizarte con nuestros servícios de desarrollo de aplicaciones corporativas.", 25 | "En Lostsys te podemos entrenar y formar a múltiples áreas de la informática corporativa como desarrollo, Data, IA o DevOps.", 26 | "En Lostsys te podemos desarrollar una tienda online para vender por todo el mundo y mas allà.", 27 | "En Lostsys te podemos desarrollar un eCommerce para vender por todo el mundo y mas allà", 28 | ], 29 | ids=["id1", "id2","id3", "id4","id5", "id6"] 30 | ) 31 | 32 | system_prompt = """ 33 | Eres un asistente de la empresa Lostsys que ayuda a sus clientes a encontrar el servicio o producto que les interesa. Sigue estas instrucciones: 34 | - Ofrece respuestas cortas y concisas de no mas de 25 palabras. 35 | - No ofrezcas consejos, productos o servícios de terceros. 36 | - Explica al cliente cosas relacionadas con en la siguiente lista JSON: """ 37 | 38 | 39 | client = AsyncOpenAI( 40 | base_url=ENDPOINT, 41 | api_key="not-needed" 42 | ) 43 | 44 | app = FastAPI() 45 | 46 | app.mount("/static", StaticFiles(directory="static"), name="static") 47 | 48 | @app.get("/", response_class=HTMLResponse) 49 | async def root( request: Request ): 50 | return RedirectResponse("/static/index.html") 51 | 52 | @app.websocket("/init") 53 | async def init( websocket: WebSocket ): 54 | await websocket.accept() 55 | 56 | try: 57 | while True: 58 | data = await websocket.receive_json() 59 | 60 | await websocket.send_json( { "action": "init_system_response" } ) 61 | response = await process_messages( data, websocket ) 62 | await websocket.send_json( { "action": "finish_system_response" } ) 63 | except (WebSocketDisconnect, ConnectionClosed): 64 | print( "Conexión cerrada" ) 65 | 66 | async def process_messages( messages, websocket ): 67 | 68 | results = collection.query( 69 | query_texts=[ messages[ -1 ]["content"] ], 70 | n_results=2 71 | ) 72 | 73 | pmsg = [ { "role": "system", "content": system_prompt + str( results["documents"][0] ) } ] 74 | print( json.dumps( pmsg + messages, indent=4) ) 75 | completion_payload = { 76 | "messages": pmsg + messages 77 | } 78 | 79 | response = await client.chat.completions.create( 80 | top_p=0.9, 81 | temperature=0.6, 82 | model=MODEL, 83 | messages=completion_payload["messages"], 84 | stream=True 85 | ) 86 | 87 | respStr = "" 88 | async for chunk in response: 89 | if (not chunk.choices[0] or 90 | not chunk.choices[0].delta or 91 | not chunk.choices[0].delta.content): 92 | continue 93 | 94 | await websocket.send_json( { "action": "append_system_response", "content": chunk.choices[0].delta.content } ) 95 | 96 | return respStr 97 | 98 | 99 | uvicorn.run(app, host="0.0.0.0", port=8000) 100 | 101 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi>=0.85.1 2 | uvicorn>=0.18.0 3 | websockets==11.0.3 4 | openai===1.55.3 5 | chromadb===0.5.23 6 | duckdb===1.1.3 7 | -------------------------------------------------------------------------------- /static/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: rgba( 247, 247, 248 ); 3 | font-family: sans-serif; 4 | margin: 0px; 5 | background-image: url( /static/img/bg3.jpg ); 6 | } 7 | 8 | #main #chatinputcontainer { 9 | margin: 15px; 10 | padding: 0.75rem; 11 | padding-left: 1rem; 12 | border: rgba( 0, 0, 0, 0.1 ) 1px solid; 13 | border-radius: 2px; 14 | box-shadow: 0 0 4px 1px rgba(143,143,143,.8); 15 | background-color: #fff; 16 | } 17 | 18 | #main #chatinputcontainer form { 19 | margin: 0px; 20 | } 21 | 22 | #main #chatinputcontainer .textarea { 23 | max-height: 200px; 24 | min-height: 36px; 25 | overflow-y: hidden; 26 | border-width: 0px; 27 | width: calc( 100% - 75px ); 28 | display: block; 29 | float: left; 30 | background-color: rgba( 0, 0, 0, 0 ); 31 | padding: 5px; 32 | color: rgba( 0, 0, 0, 0.8 ); 33 | } 34 | 35 | #main #chatinputcontainer br { 36 | clear: both; 37 | height: 1px; 38 | overflow: hidden; 39 | } 40 | 41 | #main #chatinputcontainer button { 42 | vertical-align: top; 43 | padding: 0px; 44 | padding-top: 3px; 45 | width: 48px; 46 | border: 0px; 47 | color: rgba(0,0,0,0.5); 48 | float: right; 49 | background-color: rgba(0,0,0,0); 50 | } 51 | 52 | #main #chatinputcontainer svg { 53 | width: 24px; 54 | height: 24px; 55 | } 56 | 57 | #main #linescontainer { 58 | overflow-y: scroll; 59 | height: calc( 100% - 110px ); 60 | } 61 | 62 | #main #lines { 63 | margin: 0px 15px; 64 | padding-top: 10px; 65 | } 66 | #main #lines .line { 67 | max-width: 90%; 68 | clear: both; 69 | float: left; 70 | margin: 5px 0px; 71 | padding: 10px 15px; 72 | border-radius: 5px; 73 | box-shadow: 0 0 4px 1px rgba(143,143,143,.8); 74 | color: rba( 55, 65 , 81 ); 75 | font-size: 14px; 76 | border: solid 1px rgb(117, 153, 111); 77 | background-color: rgb(217, 253, 211); 78 | } 79 | 80 | #main #lines .line.server { 81 | float: right; 82 | background-color: #fff; 83 | border: rgba( 0, 0, 0, 0.1 ) 1px solid; 84 | } 85 | 86 | /* loading-bar */ 87 | #loading { 88 | position: fixed; 89 | top: 0px; 90 | left: 0px; 91 | width: 100%; 92 | height: 100%; 93 | opacity: 0.25; 94 | background-color: #fff; 95 | z-index: 998; 96 | } 97 | 98 | .loading-bar { 99 | display: block; 100 | -webkit-animation: shift-rightwards 1s ease-in-out infinite; 101 | -moz-animation: shift-rightwards 1s ease-in-out infinite; 102 | -ms-animation: shift-rightwards 1s ease-in-out infinite; 103 | -o-animation: shift-rightwards 1s ease-in-out infinite; 104 | animation: shift-rightwards 1s ease-in-out infinite; 105 | -webkit-animation-delay: .0s; 106 | -moz-animation-delay: .0s; 107 | -o-animation-delay: .0s; 108 | animation-delay: .0s; 109 | 110 | position: fixed; 111 | top: 0; 112 | left: 0; 113 | right: 0; 114 | height: 4px; 115 | z-index: 999; 116 | background: rgb( 32, 33, 35 ); 117 | -webkit-transform: translateX(100%); 118 | -moz-transform: translateX(100%); 119 | -o-transform: translateX(100%); 120 | transform: translateX(100%); 121 | } 122 | 123 | @-webkit-keyframes shift-rightwards { 124 | 0% { 125 | -webkit-transform:translateX(-100%); 126 | -moz-transform:translateX(-100%); 127 | -o-transform:translateX(-100%); 128 | transform:translateX(-100%); 129 | } 130 | 131 | 40% { 132 | -webkit-transform:translateX(0%); 133 | -moz-transform:translateX(0%); 134 | -o-transform:translateX(0%); 135 | transform:translateX(0%); 136 | } 137 | 138 | 60% { 139 | -webkit-transform:translateX(0%); 140 | -moz-transform:translateX(0%); 141 | -o-transform:translateX(0%); 142 | transform:translateX(0%); 143 | } 144 | 145 | 100% { 146 | -webkit-transform:translateX(100%); 147 | -moz-transform:translateX(100%); 148 | -o-transform:translateX(100%); 149 | transform:translateX(100%); 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /static/img/bg3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acoronadoc/chatbot-sample/b496680c849d0fab38a5c91d77b6a7f8e3325171/static/img/bg3.jpg -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 |