diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index c82d242b..2caab5c0 100755 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -172,15 +172,16 @@ services: - API_PASSWORD=Admin!123 - OPENAPI_SPEC=/app/resources/crapi-openapi-spec.json - DEFAULT_MODEL=gpt-4o-mini - - CHROMA_PERSIST_DIRECTORY=/app/vectorstore + - CHROMA_HOST=chromadb + - CHROMA_PORT=8000 # - CHATBOT_OPENAI_API_KEY= - volumes: - - chatbot-vectors:/app/vectorstore depends_on: mongodb: condition: service_healthy crapi-identity: condition: service_healthy + chromadb: + condition: service_healthy # ports: # - "${LISTEN_IP:-127.0.0.1}:5002:5002" @@ -262,6 +263,16 @@ services: cpus: '0.3' memory: 128M + chromadb: + container_name: chromadb + image: 'chromadb/chroma:latest' + environment: + IS_PERSISTENT: 'TRUE' + volumes: + - chromadb-data:/data + # ports: + # - "${LISTEN_IP:-127.0.0.1}:8000:8000" + mailhog: user: root container_name: mailhog @@ -303,4 +314,4 @@ services: volumes: mongodb-data: postgresql-data: - chatbot-vectors: + chromadb-data: diff --git a/deploy/helm/templates/chatbot/config.yaml b/deploy/helm/templates/chatbot/config.yaml index 06db4fc1..f0cb68c2 100644 --- a/deploy/helm/templates/chatbot/config.yaml +++ b/deploy/helm/templates/chatbot/config.yaml @@ -22,7 +22,8 @@ data: MONGO_DB_NAME: {{ .Values.mongodb.config.mongoDbName }} CHATBOT_OPENAI_API_KEY: {{ .Values.openAIApiKey }} DEFAULT_MODEL: {{ .Values.chatbot.config.defaultModel | quote }} - CHROMA_PERSIST_DIRECTORY: {{ .Values.chatbot.config.chromaPersistDirectory | quote }} + CHROMA_HOST: {{ .Values.chromadb.service.name }} + CHROMA_PORT: {{ .Values.chromadb.port | quote }} API_USER: {{ .Values.chatbot.config.apiUser | quote }} API_PASSWORD: {{ .Values.chatbot.config.apiPassword | quote }} OPENAPI_SPEC: {{ .Values.chatbot.config.openapiSpec | quote }} diff --git a/deploy/helm/templates/chatbot/deployment.yaml b/deploy/helm/templates/chatbot/deployment.yaml index f58c047f..692dfa0c 100644 --- a/deploy/helm/templates/chatbot/deployment.yaml +++ b/deploy/helm/templates/chatbot/deployment.yaml @@ -57,10 +57,3 @@ spec: port: {{ .Values.chatbot.port }} initialDelaySeconds: 15 periodSeconds: 10 - volumeMounts: - - name: chatbot-vectors - mountPath: {{ .Values.chatbot.config.chromaPersistDirectory | quote }} - volumes: - - name: chatbot-vectors - persistentVolumeClaim: - claimName: {{ .Values.chatbot.storage.pvc.name }} diff --git a/deploy/helm/templates/chatbot/storage.yaml b/deploy/helm/templates/chatbot/storage.yaml deleted file mode 100644 index bace224f..00000000 --- a/deploy/helm/templates/chatbot/storage.yaml +++ /dev/null @@ -1,33 +0,0 @@ -{{- if eq .Values.chatbot.storage.type "manual" }} -apiVersion: v1 -kind: PersistentVolume -metadata: - name: {{ .Values.chatbot.storage.pv.name }} - labels: - release: {{ .Release.Name }} - {{- toYaml .Values.chatbot.storage.pv.labels | nindent 4 }} -spec: - storageClassName: {{ .Values.chatbot.storage.type }} - capacity: - storage: {{ .Values.chatbot.storage.pv.resources.storage }} - accessModes: - - ReadWriteOnce - hostPath: - path: {{ .Values.chatbot.storage.pv.hostPath }} ---- -{{- end }} -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ .Values.chatbot.storage.pvc.name }} - labels: - release: {{ .Release.Name }} - {{- toYaml .Values.chatbot.storage.pvc.labels | nindent 4 }} -spec: - {{- if ne .Values.chatbot.storage.type "default" }} - storageClassName: {{ .Values.chatbot.storage.type }} - {{- end }} - accessModes: - - ReadWriteOnce - resources: - {{- toYaml .Values.chatbot.storage.pvc.resources | nindent 4 }} diff --git a/deploy/helm/templates/chromadb/config.yaml b/deploy/helm/templates/chromadb/config.yaml new file mode 100644 index 00000000..30b8d4a1 --- /dev/null +++ b/deploy/helm/templates/chromadb/config.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.chromadb.config.name }} + labels: + release: {{ .Release.Name }} + {{- with .Values.chromadb.config.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +data: + IS_PERSISTENT: {{ .Values.chromadb.config.isPersistent | quote }} diff --git a/deploy/helm/templates/chromadb/service.yaml b/deploy/helm/templates/chromadb/service.yaml new file mode 100644 index 00000000..f9eca312 --- /dev/null +++ b/deploy/helm/templates/chromadb/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.chromadb.service.name }} + labels: + release: {{ .Release.Name }} + {{- with .Values.chromadb.service.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + ports: + - port: {{ .Values.chromadb.port }} + name: chromadb + selector: + {{- toYaml .Values.chromadb.serviceSelectorLabels | nindent 4 }} diff --git a/deploy/helm/templates/chromadb/statefulset.yaml b/deploy/helm/templates/chromadb/statefulset.yaml new file mode 100644 index 00000000..1af046ee --- /dev/null +++ b/deploy/helm/templates/chromadb/statefulset.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ .Values.chromadb.name }} + labels: + release: {{ .Release.Name }} + {{- with .Values.chromadb.statefulsetLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + serviceName: {{ .Values.chromadb.service.name }} + replicas: {{ .Values.chromadb.replicaCount }} + selector: + matchLabels: + {{- toYaml .Values.chromadb.statefulsetSelectorMatchLabels | nindent 6 }} + template: + metadata: + labels: + release: {{ .Release.Name }} + {{- toYaml .Values.chromadb.podLabels | nindent 8 }} + spec: + containers: + - name: {{ .Values.chromadb.name }} + image: {{ .Values.chromadb.image }}:{{ .Values.chromadb.version }} + imagePullPolicy: {{ .Values.chromadb.imagePullPolicy }} + ports: + - containerPort: {{ .Values.chromadb.port }} + envFrom: + - configMapRef: + name: {{ .Values.chromadb.config.name }} + volumeMounts: + - mountPath: /data + name: chromadb-data + volumes: + - name: chromadb-data + persistentVolumeClaim: + claimName: {{ .Values.chromadb.storage.pvc.name }} + \ No newline at end of file diff --git a/deploy/helm/templates/chromadb/storage.yaml b/deploy/helm/templates/chromadb/storage.yaml new file mode 100644 index 00000000..b3a1873d --- /dev/null +++ b/deploy/helm/templates/chromadb/storage.yaml @@ -0,0 +1,34 @@ +{{- if eq .Values.chromadb.storage.type "manual" }} +apiVersion: v1 +kind: PersistentVolume +metadata: + name: {{ .Values.chromadb.storage.pv.name }} + labels: + release: {{ .Release.Name }} + {{- toYaml .Values.chromadb.storage.pv.labels | nindent 4 }} +spec: + storageClassName: {{ .Values.chromadb.storage.type }} + capacity: + storage: {{ .Values.chromadb.storage.pv.resources.storage }} + accessModes: + - ReadWriteOnce + hostPath: + path: {{ .Values.chromadb.storage.pv.hostPath }} +--- +{{- end }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Values.chromadb.storage.pvc.name }} + labels: + release: {{ .Release.Name }} + {{- toYaml .Values.chromadb.storage.pvc.labels | nindent 4 }} +spec: + {{- if ne .Values.chromadb.storage.type "default" }} + storageClassName: {{ .Values.chromadb.storage.type }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + {{- toYaml .Values.chromadb.storage.pvc.resources | nindent 4 }} + diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index 94864f11..3146869e 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -326,3 +326,45 @@ apiGatewayService: app: gateway-service serviceSelectorLabels: app: gateway-service + + +chromadb: + name: chromadb + image: chromadb/chroma + version: latest + imagePullPolicy: IfNotPresent + port: 8000 + replicaCount: 1 + service: + name: chromadb + labels: + app: chromadb + config: + name: chromadb-config + labels: + app: chromadb + storage: + # type: "manual" + # pv: + # name: chromadb-pv + # labels: + # app: chromadb + # resources: + # storage: 1Gi + # hostPath: /mnt/chromadb + type: "default" + pvc: + name: chromadb-pv-claim + labels: + app: chromadb + resources: + requests: + storage: 2Gi + serviceSelectorLabels: + app: chromadb + podLabels: + app: chromadb + statefulsetLabels: + app: chromadb + statefulsetSelectorMatchLabels: + app: chromadb \ No newline at end of file diff --git a/services/chatbot/requirements.txt b/services/chatbot/requirements.txt index d4cbc666..89fc448a 100644 --- a/services/chatbot/requirements.txt +++ b/services/chatbot/requirements.txt @@ -19,4 +19,4 @@ faiss-cpu==1.11.0 psycopg2-binary uvicorn==0.35.0 fastmcp==2.10.2 -chromadb==1.0.15 \ No newline at end of file +chromadb-client==1.0.15 \ No newline at end of file diff --git a/services/chatbot/src/chatbot/chat_api.py b/services/chatbot/src/chatbot/chat_api.py index ed403157..798f1e84 100644 --- a/services/chatbot/src/chatbot/chat_api.py +++ b/services/chatbot/src/chatbot/chat_api.py @@ -10,7 +10,7 @@ get_or_create_session_id, store_api_key, store_model_name, - get_user_jwt + get_user_jwt, ) chat_bp = Blueprint("chat", __name__, url_prefix="/genai") @@ -38,6 +38,7 @@ async def init(): await store_api_key(session_id, openai_api_key) return jsonify({"message": "Initialized"}), 200 + @chat_bp.route("/model", methods=["POST"]) async def model(): session_id = await get_or_create_session_id() @@ -49,6 +50,7 @@ async def model(): await store_model_name(session_id, model_name) return jsonify({"model_used": model_name}), 200 + @chat_bp.route("/ask", methods=["POST"]) async def chat(): session_id = await get_or_create_session_id() @@ -62,7 +64,9 @@ async def chat(): id = data.get("id", uuid4().int & (1 << 63) - 1) if not message: return jsonify({"message": "Message is required", "id": id}), 400 - reply, response_id = await process_user_message(session_id, message, openai_api_key, model_name, user_jwt) + reply, response_id = await process_user_message( + session_id, message, openai_api_key, model_name, user_jwt + ) return jsonify({"id": response_id, "message": reply}), 200 diff --git a/services/chatbot/src/chatbot/chat_service.py b/services/chatbot/src/chatbot/chat_service.py index 8a0491f6..f94429e3 100644 --- a/services/chatbot/src/chatbot/chat_service.py +++ b/services/chatbot/src/chatbot/chat_service.py @@ -1,6 +1,6 @@ from uuid import uuid4 from langgraph.graph.message import Messages -from .vector_index import update_vector_index +from services.chatbot.src.chatbot.retrieverutils import add_to_chroma_collection from .extensions import db from .langgraph_agent import execute_langgraph_agent @@ -27,7 +27,9 @@ async def process_user_message(session_id, user_message, api_key, model_name, us source_message_id = uuid4().int & (1 << 63) - 1 history.append({"id": source_message_id, "role": "user", "content": user_message}) # Run LangGraph agent - response = await execute_langgraph_agent(api_key, model_name, history, user_jwt, session_id) + response = await execute_langgraph_agent( + api_key, model_name, history, user_jwt, session_id + ) print("Response", response) reply: Messages = response.get("messages", [{}])[-1] print("Reply", reply.content) @@ -35,11 +37,10 @@ async def process_user_message(session_id, user_message, api_key, model_name, us history.append( {"id": response_message_id, "role": "assistant", "content": reply.content} ) + await add_to_chroma_collection( + api_key, session_id, [{"user": user_message}, {"assistant": reply.content}] + ) # Limit chat history to last 20 messages history = history[-20:] await update_chat_history(session_id, history) - # if not os.path.exists(retrieval_index_path): - # await build_vector_index_from_chat_history(api_key) - # else: - await update_vector_index(api_key, session_id, {"user": user_message, "assistant": reply.content}) return reply.content, response_message_id diff --git a/services/chatbot/src/chatbot/config.py b/services/chatbot/src/chatbot/config.py index eb7d81f7..7f3d2ecf 100644 --- a/services/chatbot/src/chatbot/config.py +++ b/services/chatbot/src/chatbot/config.py @@ -2,7 +2,7 @@ from dotenv import load_dotenv -from .dbconnections import MONGO_CONNECTION_URI +from .dbconnections import MONGO_CONNECTION_URI, CHROMA_HOST, CHROMA_PORT load_dotenv() @@ -11,4 +11,5 @@ class Config: SECRET_KEY = os.getenv("SECRET_KEY", "super-secret") MONGO_URI = MONGO_CONNECTION_URI DEFAULT_MODEL_NAME = os.getenv("DEFAULT_MODEL", "gpt-4o-mini") - CHROMA_PERSIST_DIRECTORY = os.getenv("CHROMA_PERSIST_DIRECTORY", "/app/vectorstore") + CHROMA_HOST = CHROMA_HOST + CHROMA_PORT = CHROMA_PORT diff --git a/services/chatbot/src/chatbot/dbconnections.py b/services/chatbot/src/chatbot/dbconnections.py index b283d79c..385546f9 100644 --- a/services/chatbot/src/chatbot/dbconnections.py +++ b/services/chatbot/src/chatbot/dbconnections.py @@ -32,3 +32,6 @@ POSTGRES_PORT, POSTGRES_DB, ) + +CHROMA_HOST = os.environ.get("CHROMA_HOST", "chromadb") +CHROMA_PORT = os.environ.get("CHROMA_PORT", "8000") diff --git a/services/chatbot/src/chatbot/langgraph_agent.py b/services/chatbot/src/chatbot/langgraph_agent.py index 4ae5e450..d942cc48 100644 --- a/services/chatbot/src/chatbot/langgraph_agent.py +++ b/services/chatbot/src/chatbot/langgraph_agent.py @@ -16,34 +16,13 @@ from langgraph.graph import MessageGraph, StateGraph from langgraph.graph.message import add_messages from langgraph.prebuilt import create_react_agent +from chromadb.config import DEFAULT_TENANT, DEFAULT_DATABASE, Settings + from .extensions import postgresdb +from .config import Config from .mcp_client import get_mcp_client - - -async def get_retriever_tool(api_key): - embeddings = OpenAIEmbeddings(api_key=api_key) - if os.path.exists("faiss_index"): - vectorstore = FAISS.load_local( - "faiss_index", embeddings, allow_dangerous_deserialization=True - ) - else: - retrival_dir = os.path.join(os.path.dirname(__file__), "../../retrieval") - loader = DirectoryLoader(retrival_dir) # or PDF, Markdown, etc. - docs = loader.load() - vectorstore = FAISS.from_documents(docs, embeddings) - vectorstore.save_local("faiss_index") - retriever = vectorstore.as_retriever( - search_type="similarity", search_kwargs={"k": 3} - ) - - # ✅ Create RAG tool - retriever_tool = create_retriever_tool( - retriever, - name="crapi_rag", - description="Use this to answer questions about crAPI, its endpoints, flows, vulnerabilities, and APIs.", - ) - return retriever_tool +import chromadb async def build_langgraph_agent(api_key, model_name, user_jwt): @@ -90,13 +69,15 @@ async def build_langgraph_agent(api_key, model_name, user_jwt): mcp_tools = await mcp_client.get_tools() db_tools = toolkit.get_tools() tools = mcp_tools + db_tools - # retriever_tool = await get_retriever_tool(api_key) - # tools.append(retriever_tool) + retriever_tool = await get_retriever_tool(api_key) + tools.append(retriever_tool) agent_node = create_react_agent(model=llm, tools=tools, prompt=system_prompt) return agent_node -async def execute_langgraph_agent(api_key, model_name, messages, user_jwt, session_id=None): +async def execute_langgraph_agent( + api_key, model_name, messages, user_jwt, session_id=None +): agent = await build_langgraph_agent(api_key, model_name, user_jwt) print("messages", messages) print("Session ID", session_id) diff --git a/services/chatbot/src/chatbot/mcp_client.py b/services/chatbot/src/chatbot/mcp_client.py index c2632184..f9d8678e 100644 --- a/services/chatbot/src/chatbot/mcp_client.py +++ b/services/chatbot/src/chatbot/mcp_client.py @@ -1,5 +1,6 @@ from langchain_mcp_adapters.client import MultiServerMCPClient + def get_mcp_client(user_jwt: str | None) -> MultiServerMCPClient: headers = {} if user_jwt: @@ -13,4 +14,4 @@ def get_mcp_client(user_jwt: str | None) -> MultiServerMCPClient: "headers": headers, } } - ) \ No newline at end of file + ) diff --git a/services/chatbot/src/chatbot/retrieverutils.py b/services/chatbot/src/chatbot/retrieverutils.py new file mode 100644 index 00000000..4bea1727 --- /dev/null +++ b/services/chatbot/src/chatbot/retrieverutils.py @@ -0,0 +1,80 @@ +import os +import textwrap +from typing import Annotated, Sequence, TypedDict + +from langchain.agents.agent_toolkits import create_retriever_tool +from langchain.chains import LLMChain, RetrievalQA +from langchain.prompts import PromptTemplate +from langchain.schema import BaseMessage +from langchain.tools import Tool +from langchain_community.agent_toolkits import SQLDatabaseToolkit +from langchain_community.agent_toolkits.sql.base import create_sql_agent +from langchain_community.document_loaders import DirectoryLoader, TextLoader +from langchain_community.embeddings import OpenAIEmbeddings +from langchain_community.vectorstores import FAISS # or Chroma, Weaviate, etc. +from langchain_openai import ChatOpenAI +from langgraph.graph import MessageGraph, StateGraph +from langgraph.graph.message import add_messages +from langgraph.prebuilt import create_react_agent +from chromadb.config import DEFAULT_TENANT, DEFAULT_DATABASE, Settings + + +from .extensions import postgresdb +from .config import Config +from .mcp_client import get_mcp_client +import chromadb + +from langchain_community.embeddings import OpenAIEmbeddings +from langchain_community.vectorstores import Chroma +from langchain_core.documents import Document +from .config import Config +from chromadb.utils.embedding_functions import OpenAIEmbeddingFunction + + +async def get_chroma_collection(api_key): + chroma_client = chromadb.AsyncHttpClient( + host=Config.CHROMA_HOST, + port=Config.CHROMA_PORT, + ssl=False, + headers=None, + settings=Settings(), + tenant=DEFAULT_TENANT, + database=DEFAULT_DATABASE, + ) + + collection = chroma_client.get_or_create_collection( + name="chats", + embedding_function=OpenAIEmbeddingFunction( + api_key=api_key, + model="text-embedding-3-large", + ), + ) + return collection + + +async def add_to_chroma_collection(api_key, session_id, new_messages): + collection = await get_chroma_collection(api_key) + collection.add( + documents=[ + {"content": content, "metadata": {"session_id": session_id, "role": role}} + for role, content in new_messages.items() + ] + ) + + +async def get_retriever_tool(api_key): + collection = await get_chroma_collection(api_key) + retriever = collection.as_retriever() + retriever_tool = create_retriever_tool( + retriever, + name="chat_rag", + description=""" + Use this to answer questions based on user chat history (summarized and semantically indexed). + Use this when the user asks about prior chats, what they asked earlier, or wants a summary of past conversations. + + Use this tool when the user refers to anything mentioned before, asks for a summary of previous messages or sessions, + or references phrases like 'what I said earlier', 'things we discussed', 'my earlier question', 'until now', 'till date', 'all my conversations' or 'previously mentioned'. + The chat history is semantically indexed and summarized using vector search. + """, + ) + return retriever_tool diff --git a/services/chatbot/src/chatbot/session_service.py b/services/chatbot/src/chatbot/session_service.py index 9216d68c..53612c21 100644 --- a/services/chatbot/src/chatbot/session_service.py +++ b/services/chatbot/src/chatbot/session_service.py @@ -45,11 +45,13 @@ async def delete_api_key(session_id): {"session_id": session_id}, {"$unset": {"openai_api_key": ""}} ) + async def store_model_name(session_id, model_name): await db.sessions.update_one( {"session_id": session_id}, {"$set": {"model_name": model_name}}, upsert=True ) + async def get_model_name(session_id): doc = await db.sessions.find_one({"session_id": session_id}) if not doc: @@ -58,6 +60,7 @@ async def get_model_name(session_id): return Config.DEFAULT_MODEL_NAME return doc["model_name"] + async def get_user_jwt() -> str | None: auth = request.headers.get("Authorization", "") if auth.startswith("Bearer "): diff --git a/services/chatbot/src/chatbot/vector_index.py b/services/chatbot/src/chatbot/vector_index.py deleted file mode 100644 index 8397ea9a..00000000 --- a/services/chatbot/src/chatbot/vector_index.py +++ /dev/null @@ -1,23 +0,0 @@ -from langchain_community.embeddings import OpenAIEmbeddings -from langchain_community.vectorstores import Chroma -from langchain_core.documents import Document -from .config import Config - -async def update_vector_index(api_key, session_id, new_messages): - docs = [] - for role, content in new_messages.items(): - if content: - doc = Document( - page_content=content, - metadata={"session_id": session_id, "role": role} - ) - docs.append(doc) - - if docs: - embeddings = OpenAIEmbeddings(api_key=api_key, model="text-embedding-3-large") - vectorstore = Chroma( - embedding_function=embeddings, - persist_directory=Config.CHROMA_PERSIST_DIRECTORY - ) - vectorstore.add_documents(docs) - vectorstore.persist() \ No newline at end of file diff --git a/services/chatbot/src/mcpserver/__main__.py b/services/chatbot/src/mcpserver/__main__.py index 1e67dc86..ffd62d3f 100644 --- a/services/chatbot/src/mcpserver/__main__.py +++ b/services/chatbot/src/mcpserver/__main__.py @@ -12,4 +12,4 @@ if __name__ == "__main__": logger.info("Starting MCP server...") mcp_server_port = int(os.environ.get("MCP_SERVER_PORT", 5500)) - app.run(transport="streamable-http", host="0.0.0.0", port=mcp_server_port) + app.run(transport="streamable-http", host="0.0.0.0", port=mcp_server_port) diff --git a/services/chatbot/src/mcpserver/server.py b/services/chatbot/src/mcpserver/server.py index e0dbc29a..05630ed5 100644 --- a/services/chatbot/src/mcpserver/server.py +++ b/services/chatbot/src/mcpserver/server.py @@ -9,6 +9,7 @@ get_any_api_key, get_chat_history_retriever, ) + # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" @@ -20,6 +21,7 @@ BASE_IDENTITY_URL = f"{'https' if Config.TLS_ENABLED else 'http'}://{Config.IDENTITY_SERVICE}" API_KEY = None + def get_api_key(): global API_KEY # Try 5 times to get client auth @@ -39,9 +41,15 @@ def get_api_key(): response = client.post(auth_url, json=login_body) if response.status_code != 200: if i == MAX_ATTEMPTS - 1: - logger.error(f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}") - raise Exception(f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}") - logger.error(f"Failed to get API key in attempt {i+1}: {response.status_code} {response.text}. Sleeping for {i} seconds...") + logger.error( + f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}" + ) + raise Exception( + f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}" + ) + logger.error( + f"Failed to get API key in attempt {i+1}: {response.status_code} {response.text}. Sleeping for {i} seconds..." + ) time.sleep(i) response_json = response.json() logger.info(f"Response: {response_json}") @@ -67,32 +75,13 @@ def get_http_client(): # Create the MCP server mcp = FastMCP.from_openapi( - openapi_spec=openapi_spec, - client=get_http_client(), - name="My crAPI MCP Server" + openapi_spec=openapi_spec, client=get_http_client(), name="My crAPI MCP Server" ) -@mcp.tool(tags={"history", "search", "summary", "context"},) -async def search_chat_history(question: str) -> str: - """Answer questions based on user chat history (summarized and semantically indexed). - Use this when the user asks about prior chats, what they asked earlier, or wants a summary of past conversations. - Answer questions based on the user's prior chat history. - - Use this tool when the user refers to anything mentioned before, asks for a summary of previous messages or sessions, - or references phrases like 'what I said earlier', 'things we discussed', 'my earlier question', 'until now', 'till date', 'all my conversations' or 'previously mentioned'. - The chat history is semantically indexed and summarized using vector search.""" - - logger.info(f"search_chat_history called with: {question}") - api_key=await get_any_api_key() - if not api_key: - logger.error("API key is not available. Cannot search chat history.") - return "OpenAI API key is not available. Cannot search chat history." - retriever = await get_chat_history_retriever(api_key=api_key) - response = await retriever.ainvoke({"query": question}) - result = response["result"] - logger.info(f"RESULT: {result}") - return result - if __name__ == "__main__": mcp_server_port = int(os.environ.get("MCP_SERVER_PORT", 5500)) - mcp.run(transport="streamable-http", host="0.0.0.0", port=mcp_server_port,) + mcp.run( + transport="streamable-http", + host="0.0.0.0", + port=mcp_server_port, + ) diff --git a/services/chatbot/src/mcpserver/tool_helpers.py b/services/chatbot/src/mcpserver/tool_helpers.py index 8f8a68f6..87783ed3 100644 --- a/services/chatbot/src/mcpserver/tool_helpers.py +++ b/services/chatbot/src/mcpserver/tool_helpers.py @@ -11,36 +11,8 @@ async def get_any_api_key(): if os.environ.get("CHATBOT_OPENAI_API_KEY"): return os.environ.get("CHATBOT_OPENAI_API_KEY") doc = await db.sessions.find_one( - {"openai_api_key": {"$exists": True, "$ne": None}}, - {"openai_api_key": 1} + {"openai_api_key": {"$exists": True, "$ne": None}}, {"openai_api_key": 1} ) if doc and "openai_api_key" in doc: - return doc["openai_api_key"] + return doc["openai_api_key"] return None - -async def get_chat_history_retriever(api_key: str): - prompt_template = PromptTemplate.from_template( - """You are an assistant that summarizes chat history across sessions. - - Given the following chat excerpts: - {context} - Answer the user's question: {question} - - If the user asks for a summary, provide a coherent, high-level summary of the conversations in natural language. - If the user asks a specific question, extract and answer it from the chats. - Be detailed, accurate, and neutral.""" - ) - embeddings = OpenAIEmbeddings(api_key=api_key, model="text-embedding-3-large") - vectorstore = Chroma( - embedding_function=embeddings, - persist_directory=Config.CHROMA_PERSIST_DIRECTORY - ) - retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 5}) - qa_chain = RetrievalQA.from_chain_type( - llm=ChatOpenAI(api_key=api_key, model="gpt-4o"), - retriever=retriever, - chain_type="stuff", - chain_type_kwargs={"prompt": prompt_template, "document_variable_name": "context"}, - return_source_documents=False, - ) - return qa_chain diff --git a/services/web/package-lock.json b/services/web/package-lock.json index ff6bd40d..22aa1ff7 100644 --- a/services/web/package-lock.json +++ b/services/web/package-lock.json @@ -11,6 +11,7 @@ "@ant-design/cssinjs": "^1.21.1", "@ant-design/icons": "^4.8.3", "@ant-design/pro-components": "^2.7.15", + "@rcb-plugins/markdown-renderer": "^0.3.1", "@reduxjs/toolkit": "^2.2.7", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", @@ -27,7 +28,7 @@ "jsonwebtoken": "^9.0.2", "prop-types": "^15.8.1", "react": "^18.3.0", - "react-chatbot-kit": "^2.2.2", + "react-chatbotify": "^2.2.0", "react-dom": "^18.3.0", "react-linkify": "^1.0.0-alpha", "react-markdown": "^10.1.0", @@ -4675,6 +4676,48 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@rcb-plugins/markdown-renderer": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@rcb-plugins/markdown-renderer/-/markdown-renderer-0.3.1.tgz", + "integrity": "sha512-3HN3yLQhMWnrzhC6fdbcqeTPrTtSUQJaiIZbDmbzxlpAanfHDAiFwlVez0YY4njf9NFF4FbzNk8lZBaVLojV1g==", + "dependencies": { + "react-markdown": "^9.0.3" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.34.7" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-chatbotify": "^2.0.0-beta.38", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@rcb-plugins/markdown-renderer/node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/@redux-saga/core": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.3.0.tgz", @@ -4845,6 +4888,18 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "license": "MIT" }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -19544,21 +19599,15 @@ "node": ">=14" } }, - "node_modules/react-chatbot-kit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/react-chatbot-kit/-/react-chatbot-kit-2.2.2.tgz", - "integrity": "sha512-8p/i0KkzkhoyG2XsL6Pb6f72k9j7GYNAc5SOa4f9OZwbCD3Q34uEruNPc06qa1wZHKfT6aFna19PA2plFuO2NA==", - "license": "MIT", - "dependencies": { - "react-conditionally-render": "^1.0.2" + "node_modules/react-chatbotify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-chatbotify/-/react-chatbotify-2.2.0.tgz", + "integrity": "sha512-V/5z9cZZoAl82JqyDvTXuRN5w2tIYjV/u0ObLNvHYMIA2wY+5a42Uk8Ni7Oetrnde1M7qazhN7WDWPhLdmDPkA==", + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" } }, - "node_modules/react-conditionally-render": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/react-conditionally-render/-/react-conditionally-render-1.0.2.tgz", - "integrity": "sha512-CtjIgaLHVDSgHis3gv/PT/8EnD6GPUL8PrhUjh7DP6S5Y3p56dGu7y2nVg6pYv1kv+fGznRhRmX3assr/vRw3A==", - "license": "ISC" - }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -27293,6 +27342,35 @@ "rc-util": "^5.44.0" } }, + "@rcb-plugins/markdown-renderer": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@rcb-plugins/markdown-renderer/-/markdown-renderer-0.3.1.tgz", + "integrity": "sha512-3HN3yLQhMWnrzhC6fdbcqeTPrTtSUQJaiIZbDmbzxlpAanfHDAiFwlVez0YY4njf9NFF4FbzNk8lZBaVLojV1g==", + "requires": { + "@rollup/rollup-linux-x64-gnu": "^4.34.7", + "react-markdown": "^9.0.3" + }, + "dependencies": { + "react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "requires": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + } + } + } + }, "@redux-saga/core": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.3.0.tgz", @@ -27405,6 +27483,12 @@ } } }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "optional": true + }, "@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -37138,18 +37222,11 @@ "whatwg-fetch": "^3.6.2" } }, - "react-chatbot-kit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/react-chatbot-kit/-/react-chatbot-kit-2.2.2.tgz", - "integrity": "sha512-8p/i0KkzkhoyG2XsL6Pb6f72k9j7GYNAc5SOa4f9OZwbCD3Q34uEruNPc06qa1wZHKfT6aFna19PA2plFuO2NA==", - "requires": { - "react-conditionally-render": "^1.0.2" - } - }, - "react-conditionally-render": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/react-conditionally-render/-/react-conditionally-render-1.0.2.tgz", - "integrity": "sha512-CtjIgaLHVDSgHis3gv/PT/8EnD6GPUL8PrhUjh7DP6S5Y3p56dGu7y2nVg6pYv1kv+fGznRhRmX3assr/vRw3A==" + "react-chatbotify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/react-chatbotify/-/react-chatbotify-2.2.0.tgz", + "integrity": "sha512-V/5z9cZZoAl82JqyDvTXuRN5w2tIYjV/u0ObLNvHYMIA2wY+5a42Uk8Ni7Oetrnde1M7qazhN7WDWPhLdmDPkA==", + "requires": {} }, "react-dev-utils": { "version": "12.0.1", diff --git a/services/web/package.json b/services/web/package.json index cf0f63c0..dac13cdc 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -1,12 +1,13 @@ { "name": "crapi-web", "version": "0.1.0", - "proxy": "http://localhost:8888", + "proxy": "https://crapi.allvapps.com", "private": true, "dependencies": { "@ant-design/cssinjs": "^1.21.1", "@ant-design/icons": "^4.8.3", "@ant-design/pro-components": "^2.7.15", + "@rcb-plugins/markdown-renderer": "^0.3.1", "@reduxjs/toolkit": "^2.2.7", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", @@ -23,7 +24,7 @@ "jsonwebtoken": "^9.0.2", "prop-types": "^15.8.1", "react": "^18.3.0", - "react-chatbot-kit": "^2.2.2", + "react-chatbotify": "^2.2.0", "react-dom": "^18.3.0", "react-linkify": "^1.0.0-alpha", "react-markdown": "^10.1.0", diff --git a/services/web/src/assets/chatbot.svg b/services/web/src/assets/chatbot.svg new file mode 100644 index 00000000..f4cb67c1 --- /dev/null +++ b/services/web/src/assets/chatbot.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/services/web/src/components/bot/ActionProvider.tsx b/services/web/src/components/bot/ActionProvider.tsx deleted file mode 100644 index 5960f6b1..00000000 --- a/services/web/src/components/bot/ActionProvider.tsx +++ /dev/null @@ -1,431 +0,0 @@ -/* - * - * Licensed under the Apache License, Version 2.0 (the “License”); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an “AS IS” BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { APIService } from "../../constants/APIConstant"; -import { v4 as uuidv4 } from "uuid"; -// import { isAccessTokenValid } from "../../utils"; -import superagent from "superagent"; -import { ChatMessage } from "./MessageParser"; - -export interface ChatBotMessage { - message: string; - role: string; - id: number; - loading?: boolean; - terminateLoading?: boolean; -} - -interface State { - openapiKey: string | null; - initializing: boolean; - initializationRequired: boolean; - messages: ChatBotMessage[]; -} - -type SetStateFunc = (stateUpdater: (state: State) => State) => void; - -class ActionProvider { - private createChatBotMessage: ( - message: string, - id: number, - options?: Partial, - ) => ChatBotMessage; - private setState: SetStateFunc; - private createClientMessage: (message: string) => ChatBotMessage; - - constructor( - createChatBotMessage: ( - message: string, - id: number, - options?: Partial, - ) => ChatBotMessage, - setStateFunc: SetStateFunc, - createClientMessage: (message: string) => ChatBotMessage, - ) { - this.createChatBotMessage = createChatBotMessage; - this.setState = setStateFunc; - this.createClientMessage = createClientMessage; - } - - handleNotInitialized = (): void => { - const message = this.createChatBotMessage( - "To initialize the chatbot, please type init and press enter.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(message); - }; - - handleInitialize = (initRequired: boolean): void => { - console.log("Initialization required:", initRequired); - if (initRequired) { - this.addOpenApiKeyToState(null); - this.addInitializingToState(); - const message = this.createChatBotMessage( - "Please type your OpenAI API key and press enter.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(message); - } else { - const message = this.createChatBotMessage( - "Bot already initialized", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(message); - } - }; - - handleInitialized = (apiKey: string | null, accessToken: string): void => { - if (!apiKey) { - const message = this.createChatBotMessage( - "Please enter a valid OpenAI API key.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(message); - return; - } - localStorage.setItem("openapi_key", apiKey); - this.addOpenApiKeyToState(apiKey); - const initUrl = APIService.CHATBOT_SERVICE + "genai/init"; - superagent - .post(initUrl) - .send({ openai_api_key: apiKey }) - .set("Accept", "application/json") - .set("Content-Type", "application/json") - .set("Authorization", `Bearer ${accessToken}`) - .end((err, res) => { - if (err) { - console.log(err); - const errormessage = this.createChatBotMessage( - "Failed to initialize chatbot. Please reverify the OpenAI API key.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(errormessage); - return; - } - console.log(res); - const successmessage = this.createChatBotMessage( - "Chatbot initialized successfully. By default, GPT-4o-mini model is being used. To change chatbot's model, please type model and press enter.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(successmessage); - this.addInitializedToState(); - }); - }; - - handleModelSelection = (initRequired: boolean): void => { - console.log("Initialization required:", initRequired); - if (initRequired) { - const message = this.createChatBotMessage( - "Chatbot not initialized. To initialize the chatbot, please type init and press enter.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(message); - } else { - this.addModelSelectionToState(); - const message = this.createChatBotMessage( - `Type one of these available options and press enter:\n\n` + - `1. \`gpt-4o\` : GPT-4 Omni (fastest, multimodal, best for general use)\n\n` + - `2. \`gpt-4o-mini\` : Lighter version of GPT-4o (efficient for most tasks)\n\n` + - `3. \`gpt-4-turbo\` : GPT-4 Turbo (older but solid performance)\n\n` + - `4. \`gpt-3.5-turbo\` : GPT-3.5 Turbo (cheaper, good for lightweight tasks)\n\n` + - `5. \`gpt-3.5-turbo-16k\` : Like above but with 16k context window\n\n` + - `By default, GPT-4o-mini will be used if any invalid option is entered.`, - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(message); - } - }; - - handleModelConfirmation = ( - model_name: string | null, - accessToken: string, - ): void => { - const validModels: Record = { - "1": "gpt-4o", - "2": "gpt-4o-mini", - "3": "gpt-4-turbo", - "4": "gpt-3.5-turbo", - "5": "gpt-3.5-turbo-16k", - "gpt-4o": "gpt-4o", - "gpt-4o-mini": "gpt-4o-mini", - "gpt-4-turbo": "gpt-4-turbo", - "gpt-3.5-turbo": "gpt-3.5-turbo", - "gpt-3.5-turbo-16k": "gpt-3.5-turbo-16k", - }; - const selectedModel = model_name?.trim(); - const modelToUse = - selectedModel && validModels[selectedModel] - ? validModels[selectedModel] - : null; - - const modelUrl = APIService.CHATBOT_SERVICE + "genai/model"; - superagent - .post(modelUrl) - .send({ model_name: modelToUse }) - .set("Accept", "application/json") - .set("Content-Type", "application/json") - .set("Authorization", `Bearer ${accessToken}`) - .end((err, res) => { - if (err) { - console.log(err); - const errormessage = this.createChatBotMessage( - "Failed to set model. Please try again.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(errormessage); - return; - } - - console.log(res); - const currentModel = res.body?.model_used || modelToUse; - const successmessage = this.createChatBotMessage( - `Model has been successfully set to ${currentModel}. You can now start chatting.`, - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(successmessage); - this.addModelConfirmationToState(); - }); - }; - - handleChat = (message: string, accessToken: string): void => { - const chatUrl = APIService.CHATBOT_SERVICE + "genai/ask"; - console.log("Chat message:", message); - superagent - .post(chatUrl) - .send({ message: message }) - .set("Accept", "application/json") - .set("Content-Type", "application/json") - .set("Authorization", `Bearer ${accessToken}`) - .end((err, res) => { - console.log("Chat response:", res); - if (err) { - console.log(err); - // if status code is 4xx - if (err.status >= 400 && err.status < 500) { - const errormessage = this.createChatBotMessage( - "Failed to get response from chatbot. Please reverify the OpenAI API key.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(errormessage); - return; - } else { - const errormessage = this.createChatBotMessage( - "Failed to get response from chatbot service.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(errormessage); - return; - } - } - console.log(res); - const successmessage = this.createChatBotMessage( - res.body.message, - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(successmessage); - }); - }; - - handleHelp = (initRequired: boolean): void => { - console.log("Initialization required:", initRequired); - if (initRequired) { - const message = this.createChatBotMessage( - "To initialize the chatbot, please type init and press enter. To clear the chat context, type clear or reset and press enter.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(message); - } else { - const message = this.createChatBotMessage( - "Chat with the bot and exploit it. To change chatbot's model, please type model and press enter.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(message); - } - }; - - handleResetContext = (accessToken: string): void => { - localStorage.removeItem("chat_messages"); - this.clearMessages(); - const resetUrl = APIService.CHATBOT_SERVICE + "genai/reset"; - superagent - .post(resetUrl) - .set("Accept", "application/json") - .set("Content-Type", "application/json") - .set("Authorization", `Bearer ${accessToken}`) - .end((err, res) => { - if (err) { - console.log(err); - const errormessage = this.createChatBotMessage( - "Failed to clear chat context.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(errormessage); - return; - } - console.log(res); - const successmessage = this.createChatBotMessage( - "Chat context has been cleared.", - Math.floor(Math.random() * 65536), - { - loading: true, - terminateLoading: true, - role: "assistant", - }, - ); - this.addMessageToState(successmessage); - this.addInitializedToState(); - }); - }; - - addMessageToState = (message: ChatBotMessage): void => { - this.setState((state) => ({ - ...state, - messages: [...(state.messages || []), message], // ensure UI is updated - })); - }; - - addOpenApiKeyToState = (api_key: string | null): void => { - this.setState((state) => ({ - ...state, - openapiKey: api_key, - })); - }; - - addInitializingToState = (): void => { - this.setState((state) => ({ - ...state, - initializing: true, - })); - }; - - addInitializedToState = (): void => { - this.setState((state) => ({ - ...state, - initializing: false, - initializationRequired: false, - })); - }; - - addModelSelectionToState = (): void => { - this.setState((state) => ({ - ...state, - modelSelection: true, - })); - }; - - addModelConfirmationToState = (): void => { - this.setState((state) => ({ - ...state, - modelSelection: false, - })); - }; - - clearMessages = (): void => { - this.setState((state) => ({ - ...state, - messages: [], - })); - }; - - addChatHistoryToState = (chatHistory: ChatBotMessage[]): void => { - // Only append valid ChatBotMessage objects to messages - this.setState((state) => ({ - ...state, - messages: [...(state.messages || []), ...chatHistory], - })); - }; -} - -export default ActionProvider; diff --git a/services/web/src/components/bot/Bot.tsx b/services/web/src/components/bot/Bot.tsx index af1c3e0d..b35919aa 100644 --- a/services/web/src/components/bot/Bot.tsx +++ b/services/web/src/components/bot/Bot.tsx @@ -1,57 +1,51 @@ /* * - * Licensed under the Apache License, Version 2.0 (the “License”); + * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an “AS IS” BASIS, + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import React, { useState, useEffect } from "react"; - -import config from "./config"; +import ChatBot, { Params } from "react-chatbotify"; +import MarkdownRenderer, { + MarkdownRendererBlock, +} from "@rcb-plugins/markdown-renderer"; import { APIService } from "../../constants/APIConstant"; -import MessageParser, { ChatMessage } from "./MessageParser"; -import ActionProvider from "./ActionProvider"; -import Chatbot, { createChatBotMessage } from "react-chatbot-kit"; import { Row, Col } from "antd"; -import { Space } from "antd"; -import Icon, { +import { CloseSquareOutlined, - DeleteOutlined, ExpandAltOutlined, WechatWorkOutlined, + DeleteOutlined, } from "@ant-design/icons"; import "./chatbot.css"; -import MarkdownMessage from "./MarkdownMessage"; +import chatbotIcon from "../../assets/chatbot.svg"; -const superagent = require("superagent"); +const ChatBotIcon = () => { + return ( + Chatbot + ); +}; -const BotAvatar = (): JSX.Element => ( -
-
-
- - - -
-
-
-); +const superagent = require("superagent"); -const ChatIcon = ({ size = "26pt" }: { size?: string | number }) => ( - -); +interface ChatMessage { + id: number; + role: string; + content: string; +} interface ChatBotState { openapiKey: string | null; @@ -60,7 +54,7 @@ interface ChatBotState { accessToken: string; isLoggedIn: boolean; role: string; - messages: ChatMessage[]; // ChatBotMessage[] or IMessage[] + messages: ChatMessage[]; } interface ChatBotComponentProps { @@ -70,10 +64,9 @@ interface ChatBotComponentProps { } const ChatBotComponent: React.FC = (props) => { - // Expanded state for chatbot container const [expanded, setExpanded] = useState(false); - // Set to true so chatbot is open on UI load - const [showBot, toggleBot] = useState(true); + const [chatResetKey, setChatResetKey] = useState(0); + const helpOptions = ["Initialize", "Clear", "Help"]; const [chatbotState, setChatbotState] = useState({ openapiKey: localStorage.getItem("openapi_key"), @@ -85,294 +78,490 @@ const ChatBotComponent: React.FC = (props) => { messages: [], }); - const headerText = (): JSX.Element => { - return ( -
- -     Exploit CrapBot           -                     -   - - - -
- ); - }; - - useEffect(() => { - const fetchInit = async () => { - const stateUrl = APIService.CHATBOT_SERVICE + "genai/state"; - let initRequired = false; - let chatHistory: ChatMessage[] = []; - // Wait for the response - await superagent - .get(stateUrl) + // Handle initialization + const handleInitialization = async (apiKey: string) => { + try { + const initUrl = APIService.CHATBOT_SERVICE + "genai/init"; + const response = await superagent + .post(initUrl) .set("Accept", "application/json") .set("Content-Type", "application/json") .set("Authorization", `Bearer ${props.accessToken}`) - .then((res: any) => { - console.log("I response:", res.body); - if (res.status === 200) { - if (res.body?.initialized === "true") { - initRequired = false; - if (res.body?.chat_history) { - chatHistory = res.body?.chat_history; - setChatbotState((prev) => ({ - ...prev, - messages: chatHistory.map((msg) => ({ - role: msg.role, - content: msg.content, - id: msg.id, - })), - })); - } - } else { - initRequired = true; - } - } - }) - .catch((err: any) => { - console.log("Error prefetch: ", err); - }); - console.log("Initialization required:", initRequired); - setChatbotState((prev) => ({ - ...prev, - initializationRequired: initRequired, - })); - }; - fetchInit(); - }, []); - - const config_chatbot = { - ...config, - customComponents: { - header: headerText, - botAvatar: () => , - customButtons: ( - - ), - botChatMessage: (props?: any) => , - }, - state: chatbotState, - }; - - // Convert ChatMessage[] to IMessage[] for UI - const chatMessagesToIMessages = (messages: ChatMessage[]): IMessage[] => - messages.map((msg) => ({ - id: msg.id, - message: msg.content, - type: msg.role === "assistant" ? "bot" : "user", - })); + .send({ openai_key: apiKey }); - // Dynamic initialMessages state - const [initialMessages, setInitialMessages] = useState(null); - - useEffect(() => { - async function fetchHistory() { - const history = await fetchChatHistoryFromBackend(); // returns ChatMessage[] - setInitialMessages( - history.length > 0 - ? history.map((msg) => createChatBotMessage(msg.content, {})) - : [ - createChatBotMessage( - `Hi, Welcome to crAPI! I'm CrapBot, and I'm here to be exploited.`, - {}, - ), - ], - ); + console.log("Initialization response:", response.body); + return response.body.success || response.status === 200; + } catch (err) { + console.error("Error initializing chatbot:", err); + return false; } - fetchHistory(); - }, []); - - // Debug: log messages before rendering - console.log("messages for UI:", chatbotState.messages); - - // Canonical message type is ChatMessage (imported above) - - // Define IMessage for react-chatbot-kit compatibility - interface IMessage { - id: number; - message: string; - type: string; // required - } - - // Remount Chatbot only on clear, reset, or init - const [chatbotInstanceKey, setChatbotInstanceKey] = useState(0); - - // Convert ChatMessage[] (backend/state) <-> IMessage[] (UI) - const chatHistoryToIMessage = (history: ChatMessage[]): IMessage[] => - history.map((msg, idx) => ({ - id: idx, // number, not string - message: msg.content, - type: msg.role === "assistant" ? "bot" : "user", // always string - })); - - // Use ChatMessage for all state/history updates - const addResponseMessage = (message: ChatMessage): void => { - setChatbotState((state) => ({ - ...state, - messages: [...state.messages, message], - })); }; // Fetch chat history from backend - const fetchChatHistoryFromBackend = async (): Promise => { + const fetchChatHistory = async () => { try { - const res = await superagent - .get(APIService.CHATBOT_SERVICE + "genai/history") + const stateUrl = APIService.CHATBOT_SERVICE + "genai/state"; + const response = await superagent + .get(stateUrl) .set("Accept", "application/json") .set("Content-Type", "application/json") .set("Authorization", `Bearer ${props.accessToken}`); - console.log("Fetched chat history:", res.body); - return res.body?.chat_history || []; + + console.log("Chat history response:", response.body); + return response.body.chat_history || []; } catch (err) { - console.error("Failed to fetch chat history from backend", err); + console.error("Error fetching chat history:", err); return []; } }; - // Save messages to backend and re-fetch - const saveMessages = (messages: IMessage[]): void => { - // Update UI state immediately (optimistic UI) - setChatbotState((prev) => ({ - ...prev, - messages: iMessageToChatHistory(messages), - })); - - // Sync with backend in the background - (async () => { - const chatHistory = iMessageToChatHistory(messages); - try { - await superagent - .get(APIService.CHATBOT_SERVICE + "genai/state") - .set("Accept", "application/json") - .set("Content-Type", "application/json") - .set("Authorization", `Bearer ${props.accessToken}`) - .send({ chat_history: chatHistory }); - // Do NOT re-fetch or remount here! - } catch (err) { - console.error("Failed to save chat history to backend", err); - } - })(); - }; - - // IMessage for react-chatbot-kit UI - interface IMessage { - id: number; - message: string; - type: string; // "bot" or "user" - } - - // Convert IMessage[] (UI) to ChatMessage[] (backend) - const iMessageToChatHistory = (messages: IMessage[]): ChatMessage[] => - messages.map((msg) => ({ - role: msg.type === "bot" ? "assistant" : "user", - content: msg.message, - id: msg.id, - })); - - const loadMessages = (): IMessage[] | undefined => { - const msgs = chatHistoryToIMessage(chatbotState.messages); - return msgs.length > 0 ? msgs : undefined; - }; - - const clearHistory = async (): Promise => { + // Clear chat history + const clearChatHistory = async () => { try { + const clearUrl = APIService.CHATBOT_SERVICE + "genai/reset"; await superagent - .get(APIService.CHATBOT_SERVICE + "genai/state") + .post(clearUrl) .set("Accept", "application/json") .set("Content-Type", "application/json") .set("Authorization", `Bearer ${props.accessToken}`) - .send({ chat_history: [] }); - const latestHistory = await fetchChatHistoryFromBackend(); - setChatbotState((prev) => ({ - ...prev, - messages: latestHistory, - })); - setChatbotInstanceKey((prev) => prev + 1); + .send(); + + console.log("Chat history cleared"); + return true; } catch (err) { - console.error("Failed to clear chat history on backend", err); + console.error("Error clearing chat history:", err); + return false; } }; - const resetHistory = async (): Promise => { + // Handle user messages + const handleUserMessage = async (message: string) => { try { - await superagent - .get(APIService.CHATBOT_SERVICE + "genai/state") + const chatUrl = APIService.CHATBOT_SERVICE + "genai/ask"; + console.log("Sending message to:", chatUrl); + console.log("Message:", message); + + const response = await superagent + .post(chatUrl) .set("Accept", "application/json") .set("Content-Type", "application/json") .set("Authorization", `Bearer ${props.accessToken}`) - .send({ chat_history: [] }); - const latestHistory = await fetchChatHistoryFromBackend(); - setChatbotState((prev) => ({ - ...prev, - messages: latestHistory, - })); - setChatbotInstanceKey((prev) => prev + 1); + .send({ message }); + + console.log("API Response:", response.body); + + // Check different possible response formats + let botResponse = ""; + if (response.body.response) { + botResponse = response.body.response; + } else if (response.body.answer) { + botResponse = response.body.answer; + } else if (response.body.reply) { + botResponse = response.body.reply; + } else if (response.body.message) { + botResponse = response.body.message; + } else if (typeof response.body === "string") { + botResponse = response.body; + } else { + console.log("Unexpected response format:", response.body); + botResponse = + "I received your message but couldn't process the response format. Please try again."; + } + + console.log("Bot response to render:", botResponse); + console.log( + "Testing markdown in response:", + botResponse.includes("**") || + botResponse.includes("*") || + botResponse.includes("#"), + ); + return botResponse; } catch (err) { - console.error("Failed to reset chat history on backend", err); + console.error("Error in chat API:", err); + console.error( + "Error details:", + (err as any).response?.body || (err as any).message, + ); + return "Sorry, I encountered an error. Please try again."; } }; - return ( - - -
-
- {/* Chatbot loads chat history from backend and renders it on UI load */} - {showBot && initialMessages === null &&
Loading chat...
} - {showBot && initialMessages !== null && ( - - )} + // React Chatbotify flow configuration + const flow = { + start: { + message: "Welcome to crAPI! How can I assist you today?", + transition: { duration: 1000 }, + path: "check_initialization", + }, + check_initialization: { + transition: { duration: 0 }, + chatDisabled: true, + path: async (params: Params) => { + // Check if chatbot is already initialized + try { + const stateUrl = APIService.CHATBOT_SERVICE + "genai/state"; + const response = await superagent + .get(stateUrl) + .set("Accept", "application/json") + .set("Content-Type", "application/json") + .set("Authorization", `Bearer ${props.accessToken}`); + + const isInitialized = + response.body.initialized === "true" || + response.body.initialized === true; + + if (isInitialized) { + await params.injectMessage( + "Chatbot is already initialized! Loading chat history...", + ); + + // Fetch and display chat history + const chatHistory = await fetchChatHistory(); + console.log("Chat history:", chatHistory); + setChatbotState((prev) => ({ + ...prev, + messages: chatHistory, + initializationRequired: false, + })); + + if (chatHistory.length > 0) { + // inject all the messages in the chat history + for (const message of chatHistory) { + await params.injectMessage( + message.content, + message.role === "user" ? "user" : "bot", + ); + } + await params.injectMessage( + `Loaded ${chatHistory.length} previous messages. You can now start chatting!`, + ); + } + + return "chat"; + } else { + await params.injectMessage( + "Chatbot is not initialized. Please choose an option:", + ); + return "show_options"; + } + } catch (err) { + console.error("Error checking initialization:", err); + await params.injectMessage( + "Unable to check initialization status. Please choose an option:", + ); + return "show_options"; + } + }, + renderMarkdown: ["BOT"], + }, + show_options: { + message: "What would you like to do?", + options: helpOptions, + path: "process_options", + }, + process_options: { + transition: { duration: 0 }, + chatDisabled: true, + path: async (params: Params) => { + switch (params.userInput) { + case "Initialize": + await params.injectMessage( + "Please enter your OpenAI API key to initialize the chatbot:", + ); + return "initialize"; + case "Clear": + await params.injectMessage("Clearing the chat history..."); + const cleared = await clearChatHistory(); + if (cleared) { + await params.injectMessage("Chat history cleared successfully!"); + setChatbotState((prev) => ({ ...prev, messages: [] })); + } else { + await params.injectMessage( + "Failed to clear chat history. Please try again.", + ); + } + return "show_options"; + case "Help": + await params.injectMessage(`**crAPI Chatbot Help** + +**Available Commands:** +- **Initialize**: Set up the chatbot with your OpenAI API key +- **Clear**: Clear the chat history +- **Help**: Show this help message + +**Usage:** +1. First, initialize the chatbot with your OpenAI API key +2. Once initialized, you can ask questions about crAPI +3. Use the Clear option to reset your chat history + +What would you like to do next?`); + return "show_options"; + default: + await params.injectMessage( + "Invalid option. Please choose from the available options.", + ); + return "show_options"; + } + }, + renderMarkdown: ["BOT"], + }, + initialize: { + message: "Please paste your OpenAI API key below:", + isSensitive: true, + function: async (params: Params) => { + const apiKey = params.userInput.trim(); + + if (!apiKey) { + await params.injectMessage( + "API key cannot be empty. Please enter a valid OpenAI API key:", + ); + return; + } + + await params.injectMessage("Initializing chatbot with your API key..."); + + const success = await handleInitialization(apiKey); + + if (success) { + await params.injectMessage("✅ Chatbot initialized successfully!"); + + // Fetch chat history after successful initialization + const chatHistory = await fetchChatHistory(); + setChatbotState((prev) => ({ + ...prev, + messages: chatHistory, + initializationRequired: false, + })); + + if (chatHistory.length > 0) { + await params.injectMessage( + `Loaded ${chatHistory.length} previous messages. You can now start chatting!`, + ); + } else { + await params.injectMessage( + "Ready to chat! Ask me anything about crAPI.", + ); + } + } else { + await params.injectMessage( + "❌ Failed to initialize chatbot. Please check your API key and try again:", + ); + return; + } + }, + path: "chat", + renderMarkdown: ["BOT"], + }, + chat: { + function: async (params: Params) => { + const response = await handleUserMessage(params.userInput); + await params.injectMessage(response); + }, + renderMarkdown: ["BOT"], + path: "chat", + }, + }; + + const plugins = [MarkdownRenderer()]; + // React Chatbotify settings + const settings = { + general: { + primaryColor: "#8b5cf6", + secondaryColor: "#a855f7", + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", + embedded: false, + }, + chatHistory: { + storageKey: `chat_history`, + }, + chatWindow: { + showScrollbar: true, + showHeader: true, + showFooter: true, + showChatButton: true, + showChatInput: true, + showChatHistory: true, + showChatWindow: true, + }, + chatInput: { + placeholder: "Type your message here...", + enabledPlaceholderText: "Type your message here...", + showCharacterCount: false, + allowNewlines: true, + sendButtonStyle: { + background: "#10b981", + }, + }, + botBubble: { + showAvatar: true, + allowMarkdown: true, + animate: true, + avatar: "🤖", + }, + header: { + title: ( +
+ + crAPI ChatBot + +
+
+ ), + }, + notification: { + disabled: true, + }, + audio: { + disabled: true, + }, + chatButton: { + icon: ChatBotIcon, + }, + userBubble: { + animate: true, + showAvatar: true, + avatar: "👤", + }, + }; + + // Initialize component + useEffect(() => { + console.log("ChatBot component initialized"); + }, [props.accessToken, props.isLoggedIn]); + + return ( + + +
+ +
); diff --git a/services/web/src/components/bot/MessageParser.tsx b/services/web/src/components/bot/MessageParser.tsx deleted file mode 100644 index dde0219b..00000000 --- a/services/web/src/components/bot/MessageParser.tsx +++ /dev/null @@ -1,146 +0,0 @@ -/* - * - * Licensed under the Apache License, Version 2.0 (the “License”); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an “AS IS” BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { APIService } from "../../constants/APIConstant"; -import request from "superagent"; - -interface State { - initializationRequired?: boolean; - initializing?: boolean; - modelSelection?: boolean; - accessToken: string; - chatHistory: ChatMessage[]; -} - -export interface ChatMessage { - role: string; - content: string; - id: number; - loading?: boolean; - terminateLoading?: boolean; -} - -interface ActionProvider { - handleHelp: (initRequired: boolean) => void; - handleInitialize: (initRequired: boolean) => void; - handleResetContext: (accessToken: string) => void; - handleInitialized: ( - message: string, - accessToken: string, - chatHistory: ChatMessage[], - ) => void; - handleNotInitialized: () => void; - handleModelSelection: (initRequired: boolean) => void; - handleModelConfirmation: (message: string, accessToken: string) => void; - handleChat: (message: string, accessToken: string) => void; -} - -class MessageParser { - private actionProvider: ActionProvider; - private state: State; - - constructor(actionProvider: ActionProvider, state: State) { - this.actionProvider = actionProvider; - this.state = state; - } - - async initializationRequired(): Promise<[boolean, ChatMessage[]]> { - const stateUrl = APIService.CHATBOT_SERVICE + "genai/state"; - let initRequired = false; - let chatHistory: ChatMessage[] = []; - // Wait for the response - await request - .get(stateUrl) - .set("Accept", "application/json") - .set("Content-Type", "application/json") - .set("Authorization", `Bearer ${this.state.accessToken}`) - .then((res) => { - console.log("I response:", res.body); - if (res.status === 200) { - if (res.body?.initialized === "true") { - initRequired = false; - if (res.body?.chat_history) { - chatHistory = res.body?.chat_history; - } - } else { - initRequired = true; - } - } - }) - .catch((err) => { - console.log("Error prefetch: ", err); - }); - - console.log("Initialization required:", initRequired); - return [initRequired, chatHistory]; - } - - async parse(message: string): Promise { - console.log("State:", this.state); - console.log("Message:", message); - const message_l = message.toLowerCase(); - if (this.state?.initializationRequired === undefined) { - const [initRequired, chatHistory] = await this.initializationRequired(); - this.state.initializationRequired = initRequired; - this.state.chatHistory = chatHistory; - console.log("State check:", this.state); - } - if (message_l === "help") { - const [initRequired, chatHistory] = await this.initializationRequired(); - this.state.initializationRequired = initRequired; - this.state.chatHistory = chatHistory; - console.log("State help:", this.state); - return this.actionProvider.handleHelp(this.state.initializationRequired); - } else if (message_l === "init" || message_l === "initialize") { - const [initRequired, chatHistory] = await this.initializationRequired(); - this.state.initializationRequired = initRequired; - this.state.chatHistory = chatHistory; - console.log("State init:", this.state); - return this.actionProvider.handleInitialize( - this.state.initializationRequired, - ); - } else if (message_l === "model" || message_l === "models") { - const [initRequired, chatHistory] = await this.initializationRequired(); - this.state.initializationRequired = initRequired; - this.state.chatHistory = chatHistory; - console.log("State help:", this.state); - return this.actionProvider.handleModelSelection( - this.state.initializationRequired, - ); - } else if ( - message_l === "clear" || - message_l === "reset" || - message_l === "restart" - ) { - return this.actionProvider.handleResetContext(this.state.accessToken); - } else if (this.state.initializing) { - return this.actionProvider.handleInitialized( - message, - this.state.accessToken, - this.state.chatHistory, - ); - } else if (this.state.initializationRequired) { - return this.actionProvider.handleNotInitialized(); - } else if (this.state.modelSelection) { - return this.actionProvider.handleModelConfirmation( - message, - this.state.accessToken, - ); - } - - return this.actionProvider.handleChat(message, this.state.accessToken); - } -} - -export default MessageParser; diff --git a/services/web/src/components/bot/chatbot.css b/services/web/src/components/bot/chatbot.css index 68a9dd16..fdeb5b5d 100644 --- a/services/web/src/components/bot/chatbot.css +++ b/services/web/src/components/bot/chatbot.css @@ -1,298 +1,145 @@ .app-chatbot-container { position: fixed; - bottom: 0; - right: -100; + bottom: var(--spacing-lg); + left: var(--spacing-lg); display: inline-block; + z-index: 1000; } -/* .app-chatbot-container.expanded { - width: 50vw; - height: 100vh; - border-radius: 0; - box-shadow: 2px 0 16px rgba(0,0,0,0.18); -} */ .expand-chatbot-btn { - background: #2898ec; - color: #fff; border: none; - border-radius: 50%; - width: 12px; - height: 12px; - font-size: 1.5rem; - cursor: pointer; - box-shadow: 0 2px 6px rgba(0,0,0,0.13); + outline: none; + position: absolute; + right: 40px; + top: 10px; + margin-right: 5px; + border-radius: 8px; + padding: 8px 12px; + color: rgb(232, 234, 237); + font-weight: 1000; + width: 32px; + height: 32px; + font-size: 30px; display: flex; align-items: center; justify-content: center; - transition: background 0.2s; + transition: all 0.2s ease; + background: transparent; } -.toggle-chatbot-btn { - background: #2898ec; - color: #fff; + +.expand-chatbot-btn:hover { + color: #ffffff; + transform: scale(1.05); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + opacity: 1; + background-color: rgba(0, 0, 0, 0.1); +} + +.expand-chatbot-btn:focus { + outline: none; border: none; - border-radius: 50%; - width: 12px; - height: 12px; - font-size: 1.5rem; - cursor: pointer; - box-shadow: 0 2px 6px rgba(0,0,0,0.13); +} + +/* Delete chat button */ +.delete-chat-btn { + border: none; + outline: none; + position: absolute; + right: 80px; + top: 10px; + margin-right: 5px; + border-radius: 8px; + padding: 8px 12px; + color: rgb(232, 234, 237); + font-weight: 1000; + width: 32px; + height: 32px; + font-size: 30px; display: flex; align-items: center; justify-content: center; - transition: background 0.2s; + transition: all 0.2s ease; + background: transparent; } -.expand-chatbot-btn:hover { - background: #1768b3; +.delete-chat-btn:hover { + color: #ffffff; + transform: scale(1.05); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + opacity: 1; + background-color: rgba(0, 0, 0, 0.1); } -.react-chatbot-kit-user-chat-message-container { - display: flex; - margin: 15px 0; - justify-content: flex-end; - } - - .react-chatbot-kit-user-avatar-container { - width: 40px; - height: 40px; - border-radius: 50%; - margin-left: 12.5px; - background-color: #3d4e8d; - display: flex; - justify-content: center; - align-items: center; - } - - .react-chatbot-kit-user-avatar-icon { - fill: #fff; - width: 15px; - height: 15px; - } - - .react-chatbot-kit-chat-bot-avatar-container { - width: 40px; - height: 40px; - border-radius: 50%; - font-size: 1.2rem; - background-color: #c1c2c0; - display: flex; - justify-content: center; - align-items: center; - } - - .react-chatbot-kit-bot-avatar-icon { - fill: #001529; - width: 15px; - height: 15px; - } - - .react-chatbot-kit-chat-bot-avatar-letter { - color: #1d1d1d; - margin: 0; - padding: 0; - } - - .react-chatbot-kit-user-chat-message { - background-color: #f1f1f1; - padding: 10px; - border-radius: 5px; - font-size: 0.9rem; - color: #585858; - font-weight: medium; - position: relative; - text-align: left; - } - - .react-chatbot-kit-user-chat-message-arrow { - width: 0; - height: 0; - border-top: 8px solid transparent; - border-bottom: 8px solid transparent; - border-left: 8px solid #f1f1f1; - position: absolute; - right: -7px; - top: 13px; - } - - .react-chatbot-kit-chat-bot-message-container { - display: flex; - margin: 15px 0; - justify-content: flex-start; - } - - .react-chatbot-kit-chat-bot-message { - background-color: #ebf1f8; - padding: 10px; - border-radius: 5px; - font-size: 0.9rem; - color: #691515; - font-weight: medium; - position: relative; - width: 95%; - margin-left: 12.5px; - margin-right: 12.5px; - text-align: left; - } - - .react-chatbot-kit-chat-bot-message-arrow { - width: 0; - height: 0; - border-top: 8px solid transparent; - border-bottom: 8px solid transparent; - border-right: 8px solid #ebf1f8; - position: absolute; - left: -7px; - top: 13px; - } - - .react-chatbot-kit-chat-bot-loading-icon-container { - height: 17px; - width: 25px; - } - - .chatbot-loader-container { - display: inline-block; - width: 20%; - justify-content: center; - } - - #chatbot-loader #chatbot-loader-dot1 { - animation: load 1s infinite; - } - - #chatbot-loader #chatbot-loader-dot2 { - animation: load 1s infinite; - animation-delay: 0.2s; - } - - #chatbot-loader #chatbot-loader-dot3 { - animation: load 1s infinite; - animation-delay: 0.4s; - } - - @keyframes load { - 0% { - opacity: 0; - } - 50% { - opacity: 1; - } - 100% { - opacity: 0; - } - } - - .react-chatbot-kit-chat-container { - position: absolute; - bottom: 0; - width: 30vw; - z-index: 1; - } - - .app-chatbot-container.expanded .react-chatbot-kit-chat-container { - width: 50vw; - } - - .react-chatbot-kit-chat-inner-container { - height: 80vh; - background-color: #fff; - border-radius: 3px; - border-radius: 5px; - display: flex; - flex-direction: column; - overflow: hidden; - } - - .app-chatbot-container.expanded .react-chatbot-kit-chat-inner-container { - height: 100vh; - } - - .react-chatbot-kit-chat-header { - border-top-right-radius: 5px; - border-top-left-radius: 5px; - background-color: #efefef; - font-family: Arial; - display: block; - align-items: center; - font-size: 0.85rem; - color: #514f4f; - padding: 12.5px; - font-weight: bold; - } - - .react-chatbot-kit-chat-input-container { - position: relative; - width: 100%; - display: flex; - flex-shrink: 0; - } - - .react-chatbot-kit-chat-message-container { - padding: 0 17.5px 10px 17.5px; - overflow-y: auto; - flex: 1 1 auto; - min-height: 0; - } +.delete-chat-btn:focus { + outline: none; + border: none; +} - .react-chatbot-kit-chat-input { - width: 100%; - padding: 12.5px; - border: none; - font-size: 0.85rem; - border-top: 1px solid #d8d8d8; - border-bottom-left-radius: 5px; - } +.delete-chat-btn:active { + transform: scale(0.95); +} - .react-chatbot-kit-chat-input-form { - width: 100%; - display: flex; - } - .react-chatbot-kit-chat-input::placeholder { - color: #585858; - } +.expand-chatbot-btn:after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 0; + height: 0; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 50%; + opacity: 0; + transition: width 0.2s ease-out, height 0.2s ease-out, opacity 0.2s ease-out; +} - .react-chatbot-kit-chat-btn-send { - background-color: #2898ec; - width: 100px; - border: none; - color: #fff; - border-bottom-right-radius: 5px; - } - .react-chatbot-kit-chat-btn-send-icon { - fill: #fff; - width: 15px; - margin: 0 auto; - } +/* Enhanced chatbot trigger button */ +.app-chatbot-button { + position: fixed; + bottom: var(--spacing-lg); + left: var(--spacing-lg); + width: 50px; + height: 50px; + border-radius: 50%; + background: linear-gradient(135deg, #8b5cf6, #a855f7); + border: none; + color: #ffffff; + font-size: 24px; + cursor: pointer; + box-shadow: 0 8px 24px rgba(139, 92, 246, 0.3); + transition: all var(--transition-normal); + z-index: 999; + display: flex; + align-items: center; + justify-content: center; +} - .react-chatbot-kit-error { - background-color: #fff; - border-radius: 3px; - padding: 15px; - } +.app-chatbot-button:hover { + transform: scale(1.1) rotate(5deg); + box-shadow: 0 12px 32px rgba(139, 92, 246, 0.4); + background: linear-gradient(135deg, #7c3aed, #9333ea); +} - .react-chatbot-kit-error-container { - width: 260px; - } +.app-chatbot-button:active { + transform: scale(0.95); +} - .react-chatbot-kit-error-header { - font-size: 1.3rem; - color: #1d1d1d; - margin-bottom: 30px; - } +/* Expanded chatbot window styling */ +.app-chatbot-container.expanded .rcb-chat-window { + width: max(50vw, 500px) !important; + height: 90vh !important; + min-height: 50vh !important; + min-width: 500px !important; +} - .react-chatbot-kit-error-docs { - display: block; - margin: 25px auto; - color: rgb(56, 104, 139); - padding: 8px; - border: 1px solid rgb(40, 152, 236); - width: 130px; - text-align: center; - text-decoration: none; - font-size: 1rem; - } +.rcb-bot-avatar { + background: fixed; +} +.rcb-chat-footer-container { + display: none; +} diff --git a/services/web/src/components/bot/config.tsx b/services/web/src/components/bot/config.tsx deleted file mode 100644 index 59c9b566..00000000 --- a/services/web/src/components/bot/config.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * - * Licensed under the Apache License, Version 2.0 (the “License”); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an “AS IS” BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createChatBotMessage } from "react-chatbot-kit"; - -const botName: string = "CrapBot"; - -interface Config { - initialMessages: any[]; - botName: string; - state: Record; - customStyles: { - botMessageBox: { - backgroundColor: string; - }; - chatButton: { - backgroundColor: string; - }; - }; -} - -const config: Config = { - initialMessages: [ - createChatBotMessage( - `Hi, Welcome to crAPI! I'm ${botName}, and I'm here to be exploited.`, - {}, - ), - ], - botName, - state: {}, - customStyles: { - botMessageBox: { - backgroundColor: "#376B7E", - }, - chatButton: { - backgroundColor: "#5ccc9d", - }, - }, -}; - -export default config; diff --git a/services/web/src/components/dashboard/dashboard.css b/services/web/src/components/dashboard/dashboard.css index 7da104d5..b1329c3b 100644 --- a/services/web/src/components/dashboard/dashboard.css +++ b/services/web/src/components/dashboard/dashboard.css @@ -7,8 +7,10 @@ } .vehicle-desc { - /* text-align: center; */ - background-color: #f0f0f0; + background: var(--bg-component); + border-radius: var(--border-radius-lg); + box-shadow: var(--shadow-light); + border: 1px solid var(--border-light); } .vehicle-desc .ant-descriptions-view { @@ -16,40 +18,163 @@ } .vehicle-desc .ant-descriptions-item-label { - background: none; - width: 30%; + background: var(--bg-secondary); + width: 35%; + font-weight: var(--font-weight-medium); + color: var(--text-secondary); + border-radius: var(--border-radius-sm) 0 0 var(--border-radius-sm); } +.vehicle-desc .ant-descriptions-item-content { + background: var(--bg-primary); + font-weight: var(--font-weight-medium); + color: var(--text-primary); + border-radius: 0 var(--border-radius-sm) var(--border-radius-sm) 0; +} .dashboard-header { padding-left: 0; padding-right: 0; + background: var(--bg-component); + border-radius: var(--border-radius-xl); + box-shadow: var(--shadow-light); + margin-bottom: var(--spacing-lg); + border: 1px solid var(--border-light); +} + +.dashboard-header .ant-page-header-heading-title { + font-size: var(--font-size-xxl); + font-weight: var(--font-weight-bold); + background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-left: 10px; + margin-right: 10px; +} + +/* Enhanced vehicle card */ +.vehicle-card { + background: var(--bg-component); + border-radius: var(--border-radius-xl); + box-shadow: var(--shadow-medium); + border: 1px solid var(--border-light); + overflow: hidden; + transition: all var(--transition-normal); + position: relative; +} + +.vehicle-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); } + +.vehicle-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-heavy); +} + +.vehicle-card .ant-card-meta-title { + margin-bottom: var(--spacing-md); +} + +.vehicle-card .ant-card-body { + padding: var(--spacing-xl); +} + .refresh-loc-btn { - width: 400px; - /* left: 30%; */ + width: 100%; + max-width: 400px; + height: 48px; + border-radius: var(--border-radius-lg); + font-weight: var(--font-weight-medium); + background: linear-gradient(45deg, var(--accent-color), var(--accent-hover)); + border: none; + color: var(--text-inverse); + box-shadow: var(--shadow-medium); + transition: all var(--transition-normal); + margin-top: var(--spacing-md); +} + +.refresh-loc-btn:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-heavy); + background: linear-gradient(45deg, var(--accent-hover), var(--accent-color)); } .map-iframe { border: 0; + border-radius: var(--border-radius-lg); + box-shadow: var(--shadow-light); + transition: all var(--transition-normal); +} + +.map-iframe:hover { + box-shadow: var(--shadow-medium); } .alert-msg-box { margin: auto; text-align: center; + padding: var(--spacing-xl); +} + +.alert-msg-box .ant-alert { + border-radius: var(--border-radius-xl); + border: 2px solid var(--warning-color); + background: linear-gradient(135deg, rgba(250, 173, 20, 0.1), rgba(250, 173, 20, 0.05)); + box-shadow: var(--shadow-medium); } .alert-msg-box .btn { - color: blue; + color: var(--primary-color); background-color: transparent; border: none; + font-weight: var(--font-weight-medium); + text-decoration: underline; + transition: all var(--transition-normal); + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--border-radius-sm); +} + +.alert-msg-box .btn:hover { + color: var(--primary-hover); + background-color: var(--primary-light); + transform: translateY(-1px); } .alert-header { - font-size: larger; - font-weight: bold; + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + color: var(--text-primary); } .alert-msg { - font-size: large; + font-size: var(--font-size-md); + color: var(--text-secondary); + line-height: 1.6; +} + +/* Enhanced button styles for dashboard */ +.dashboard-header .ant-btn { + height: 40px; + border-radius: var(--border-radius-lg); + font-weight: var(--font-weight-medium); + border: none; + box-shadow: var(--shadow-light); + transition: all var(--transition-normal); +} + +.dashboard-header .ant-btn-primary { + background: linear-gradient(45deg, var(--primary-color), var(--primary-hover)); +} + +.dashboard-header .ant-btn:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-medium); } diff --git a/services/web/src/components/forum/forum.tsx b/services/web/src/components/forum/forum.tsx index 4efcbb69..75ff2462 100644 --- a/services/web/src/components/forum/forum.tsx +++ b/services/web/src/components/forum/forum.tsx @@ -107,15 +107,15 @@ const Forum: React.FC = (props) => { ]} /> - + {posts.map((post) => ( - + handlePostClick(post.id)}> - + {post.author.nickname} @@ -132,25 +132,29 @@ const Forum: React.FC = (props) => { ))} - - - + + + + + + + diff --git a/services/web/src/components/forum/style.css b/services/web/src/components/forum/style.css index 0c9ac230..67d850b6 100644 --- a/services/web/src/components/forum/style.css +++ b/services/web/src/components/forum/style.css @@ -1,3 +1,279 @@ +/* Modern Forum Page Styling */ +.page-header { + padding-left: 0; + padding-right: 0; + background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); + border-radius: var(--border-radius-lg); + margin-bottom: var(--spacing-lg); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +/* Forum Post Cards */ +.ant-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: var(--border-radius-xl); + box-shadow: var(--shadow-elevation-medium); + transition: all 0.3s ease; + overflow: hidden; + position: relative; + margin-bottom: var(--spacing-lg); + cursor: pointer; +} + +.ant-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #8b5cf6, #a855f7, #c084fc); + opacity: 0; + transition: opacity 0.3s ease; +} + +.ant-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-elevation-high); + border-color: rgba(139, 92, 246, 0.3); +} + +.ant-card:hover::before { + opacity: 1; +} + +.ant-card .ant-card-body { + padding: var(--spacing-xl); +} + +/* Post Meta Information */ +.ant-card-meta { + margin-bottom: var(--spacing-lg); +} + +.ant-card-meta .ant-card-meta-title { + font-size: 18px !important; + font-weight: 700 !important; + color: #1f2937 !important; + line-height: 1.4 !important; + margin-bottom: var(--spacing-sm) !important; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + max-height: 2.8em; +} + +.ant-card-meta .ant-card-meta-avatar { + margin-right: var(--spacing-md); +} + +.ant-card-meta .ant-avatar { + border: 3px solid rgba(255, 255, 255, 0.8); + box-shadow: var(--shadow-elevation-medium); + transition: all 0.3s ease; +} + +.ant-card:hover .ant-avatar { + transform: scale(1.1); + box-shadow: var(--shadow-elevation-high); +} + +/* Post Details */ +.ant-descriptions { + margin-bottom: var(--spacing-md); +} + +.ant-descriptions-item-label { + font-weight: 600 !important; + color: #6b7280 !important; + font-size: 13px !important; +} + +.ant-descriptions-item-content { + font-weight: 500 !important; + color: #374151 !important; + font-size: 13px !important; +} + +/* Post Content */ .post-content { - margin-top: 10px; + margin-top: var(--spacing-md); + color: #4b5563; + line-height: 1.6; + font-size: 14px; +} + +.post-content .ant-typography { + margin-bottom: var(--spacing-sm); + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 4; + line-clamp: 4; + -webkit-box-orient: vertical; + max-height: 6.4em; +} + +.post-content p { + margin-bottom: var(--spacing-xs) !important; + color: #6b7280; +} + +/* New Post Button */ +.page-header .ant-btn { + background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%); + border: none; + border-radius: var(--border-radius-lg); + font-weight: 600; + font-size: 16px; + height: 48px; + padding: 0 var(--spacing-xl); + box-shadow: var(--shadow-elevation-medium); + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.page-header .ant-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s; +} + +.page-header .ant-btn:hover::before { + left: 100%; +} + + +.page-header .ant-btn .anticon { + margin-right: var(--spacing-sm); +} + +/* Pagination */ +.ant-row[justify="center"] { + margin-top: var(--spacing-xl); + gap: var(--spacing-lg); +} + +.ant-row[justify="center"] .ant-btn { + height: 48px; + padding: 0 var(--spacing-xl); + background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); + backdrop-filter: blur(10px); + border: 1px solid rgba(139, 92, 246, 0.3); + border-radius: var(--border-radius-lg); + font-weight: 600; + transition: all 0.3s ease; + box-shadow: var(--shadow-elevation-low); +} + +.ant-row[justify="center"] .ant-btn:disabled { + background: rgba(156, 163, 175, 0.1); + border-color: rgba(156, 163, 175, 0.2); + color: #9ca3af; +} + +/* Layout Grid - Single Column */ +.ant-row[gutter] .ant-col { + width: 100%; + max-width: 100%; +} + +/* Single Row Layout Adjustments */ +.ant-card { + max-width: 100%; + margin: 0 auto; +} + +.ant-card .ant-card-body { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .ant-card .ant-card-body { + padding: var(--spacing-lg); + } +} + +@media (max-width: 768px) { + .page-header { + margin-bottom: var(--spacing-md); + } + + .ant-card { + margin-bottom: var(--spacing-md); + } + + .ant-card .ant-card-body { + padding: var(--spacing-md); + } + + .ant-card-meta .ant-card-meta-title { + font-size: 16px !important; + } + + .ant-descriptions-item-label, + .ant-descriptions-item-content { + font-size: 12px !important; + } + + .post-content { + font-size: 13px; + } + + .page-header .ant-btn { + height: 44px; + font-size: 15px; + padding: 0 var(--spacing-lg); + } + + .ant-row[justify="center"] { + flex-direction: column; + align-items: center; + gap: var(--spacing-md); + } + + .ant-row[justify="center"] .ant-btn { + width: 200px; + } +} + +@media (max-width: 576px) { + .page-header .ant-page-header-heading-extra { + margin-top: var(--spacing-md); + margin-left: 10px; + margin-right: 10px; + } + + .page-header .ant-btn { + width: 100%; + } + + .ant-card-meta .ant-card-meta-title { + font-size: 15px !important; + -webkit-line-clamp: 3; + line-clamp: 3; + max-height: 4.2em; + } + + .post-content .ant-typography { + -webkit-line-clamp: 3; + line-clamp: 3; + max-height: 4.8em; + } } diff --git a/services/web/src/components/layout/layout.css b/services/web/src/components/layout/layout.css index ccb7acfa..bef8977e 100644 --- a/services/web/src/components/layout/layout.css +++ b/services/web/src/components/layout/layout.css @@ -1,5 +1,29 @@ .layout-content { - padding: 0 50px; + padding: var(--spacing-sm) var(--spacing-sm); + background: var(--bg-layout); + min-height: calc(100vh - 56px); + position: relative; + min-width: 50vw; +} + +.layout-content::before { + content: ''; + position: fixed; + top: 56px; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 50%, rgba(24, 144, 255, 0.03) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(114, 46, 209, 0.03) 0%, transparent 50%), + radial-gradient(circle at 40% 80%, rgba(19, 194, 194, 0.03) 0%, transparent 50%); + pointer-events: none; + z-index: 0; +} + +.layout-content > * { + position: relative; + z-index: 1; } .spinner { @@ -12,4 +36,25 @@ body > iframe { display: none; +} + +/* Page container styles */ +.page-container { + background: transparent; + padding: var(--spacing-lg) 0; +} + +.page-container .ant-page-header-heading-title { + padding: 0; + margin-left: 10px; + margin-right: 10px; +} + +.page-container .ant-page-header-heading-extra { + margin-left: 10px; + margin-right: 10px; +} + +.page-container .ant-layout-content { + background: transparent; } \ No newline at end of file diff --git a/services/web/src/components/login/login.css b/services/web/src/components/login/login.css index fe675d93..714bc612 100644 --- a/services/web/src/components/login/login.css +++ b/services/web/src/components/login/login.css @@ -1,37 +1,164 @@ .container { display: flex; - height: 91vh; + align-items: center; + justify-content: center; + max-height: 100vh; width: 100%; + background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + padding: var(--spacing-sm); + position: relative; +} + +.container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.3; } .alternative-style { - color: #262c6f; - font-size: 12px; + color: var(--primary-color); + font-size: var(--font-size-sm); cursor: pointer; - display: flex; - float: right; - box-sizing: border-box; - padding-bottom: 2%; + display: inline-block; + padding: var(--spacing-sm) 0; border: none; background-color: transparent; + text-decoration: none; + font-weight: var(--font-weight-medium); + transition: all var(--transition-normal); + border-radius: var(--border-radius-sm); +} + +.alternative-style:hover { + color: var(--primary-hover); + text-decoration: underline; + transform: translateY(-1px); } + .spinner { max-height: none !important; } + .error-message { - text-align: left; - color: red; - font-size: 12px; - padding: 15px 0; + text-align: center; + color: var(--error-color); + font-size: var(--font-size-sm); + padding: var(--spacing-md) 0; + background-color: rgba(255, 77, 79, 0.1); + border: 1px solid rgba(255, 77, 79, 0.2); + border-radius: var(--border-radius-md); + margin: var(--spacing-sm) 0; } .form-card { - border-radius: 5px; - width: 30%; + border-radius: var(--border-radius-xl); + width: 100%; + max-width: 400px; margin: auto; - min-width: 400px; + background: var(--bg-primary); + box-shadow: var(--shadow-heavy); + border: 1px solid rgba(255, 255, 255, 0.2); + backdrop-filter: blur(20px); + position: relative; + z-index: 1; + overflow: hidden; +} + +.form-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--primary-color), var(--accent-color), var(--secondary-color)); +} + +.form-card .ant-card-head { + background: transparent; + border-bottom: 1px solid var(--border-light); + text-align: center; +} + +.form-card .ant-card-head-title { + font-size: var(--font-size-xxl); + font-weight: var(--font-weight-bold); + color: var(--text-primary); + background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.form-card .ant-card-body { + padding: var(--spacing-xl); } .form-button { width: 100%; + height: 48px; + font-size: var(--font-size-md); + font-weight: var(--font-weight-medium); + border-radius: var(--border-radius-lg); + background: linear-gradient(45deg, var(--primary-color), var(--primary-hover)); + border: none; + box-shadow: var(--shadow-medium); + transition: all var(--transition-normal); +} + +.form-button:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-heavy); + background: linear-gradient(45deg, var(--primary-hover), var(--primary-active)); +} + +.form-button:active { + transform: translateY(0); +} + +/* Enhanced form inputs */ +.form-card .ant-input { + height: 48px; + border-radius: var(--border-radius-lg); + border: 2px solid var(--border-light); + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--font-size-md); + transition: all var(--transition-normal); +} + +.form-card .ant-input:focus, +.form-card .ant-input-focused { + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1); +} + +.form-card .ant-input-password { + border-radius: var(--border-radius-lg); +} + +.form-card .ant-form-item { + margin-bottom: var(--spacing-lg); +} + +.form-card .ant-form-item-label { + font-weight: var(--font-weight-medium); + color: var(--text-primary); +} + +/* Action buttons container */ +.form-actions { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + margin-top: var(--spacing-lg); +} + +.form-actions .alternative-style { + text-align: center; + padding: var(--spacing-sm); } diff --git a/services/web/src/components/login/login.tsx b/services/web/src/components/login/login.tsx index 177935f0..7d8a9a6e 100644 --- a/services/web/src/components/login/login.tsx +++ b/services/web/src/components/login/login.tsx @@ -78,24 +78,28 @@ const Login: React.FC = ({ - - {hasErrored &&
{errorMessage}
} - - +
+ + {hasErrored && ( +
{errorMessage}
+ )} + + +
diff --git a/services/web/src/components/navBar/nav.css b/services/web/src/components/navBar/nav.css index d6bd546b..c2848774 100644 --- a/services/web/src/components/navBar/nav.css +++ b/services/web/src/components/navBar/nav.css @@ -2,42 +2,101 @@ display: flex; float: right; align-items: center; - justify-content: space-between; - font-weight: 600; - color: white; + gap: var(--spacing-sm); + font-weight: var(--font-weight-medium); + color: var(--text-inverse); + margin-left: auto; } .top-nav-left { display: flex; float: left; align-items: center; - justify-content: space-between; - font-weight: 600; - color: white; + gap: var(--spacing-xl); + font-weight: var(--font-weight-medium); + color: var(--text-inverse); } .avatar { - background-color: white; + background-color: var(--bg-primary); cursor: pointer; + border: 2px solid var(--primary-light); + transition: all var(--transition-normal); +} + +.avatar:hover { + border-color: var(--accent-color); + transform: scale(1.05); +} + +.profile-dropdown-trigger .avatar { + cursor: inherit; + border: 2px solid rgba(255, 255, 255, 0.3); +} + +.profile-dropdown-trigger:hover .avatar { + border-color: rgba(255, 255, 255, 0.6); + transform: scale(1.05); } .avatarContainer { align-items: center; justify-content: center; display: flex; - height: 48px; - width: 48px; + height: 44px; + width: 44px; border-radius: 50%; - transition: background-color 0.25s; + transition: all var(--transition-normal); + position: relative; + flex-shrink: 0; +} + +.profile-dropdown-trigger .avatarContainer { + height: 40px; + width: 40px; +} + +.profile-dropdown-trigger .avatarContainer:hover { + background-color: transparent; } .avatarContainer:hover { - background-color: #1890ff; + background-color: rgba(255, 255, 255, 0.1); +} + +.avatarContainer::before { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + border-radius: 50%; + background: linear-gradient(45deg, var(--primary-color), var(--accent-color)); + opacity: 0; + transition: opacity var(--transition-normal); + z-index: -1; +} + +.avatarContainer:hover::before { + opacity: 0.3; } .logo-text { - font-size: 15px; - font-weight: bold; + font-size: var(--font-size-xl); + font-weight: var(--font-weight-bold); + background: linear-gradient(45deg, var(--text-inverse), var(--accent-light)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + cursor: pointer; + transition: all var(--transition-normal); + letter-spacing: 0.5px; +} + +.logo-text:hover { + transform: scale(1.05); + filter: brightness(1.2); } .notification { @@ -85,10 +144,115 @@ cursor: pointer; } +/* Enhanced Profile Dropdown Trigger */ +.profile-dropdown-trigger { + display: flex; + align-items: center; + gap: var(--spacing-sm); + cursor: pointer; + padding: var(--spacing-xs) var(--spacing-md); + border-radius: var(--border-radius-lg); + transition: all var(--transition-normal); + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.15); + min-width: 120px; + position: relative; + overflow: hidden; +} + +.profile-dropdown-trigger::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); + transition: left var(--transition-normal); +} + +.profile-dropdown-trigger:hover::before { + left: 100%; +} + +.profile-dropdown-trigger:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.3); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.profile-dropdown-trigger:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); +} + +.dropdown-info { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 2px; + flex: 1; + min-width: 0; +} + +.user-name { + color: var(--text-inverse); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100px; + line-height: 1.2; +} + +.dropdown-arrow { + color: var(--text-inverse); + font-size: 12px; + opacity: 0.8; + transition: all var(--transition-normal); + margin-left: auto; + align-self: center; +} + +.profile-dropdown-trigger:hover .dropdown-arrow { + opacity: 1; + transform: translateY(-1px); +} + +/* Legacy nav-items for any remaining usage */ .nav-items { - color: white; - margin-right: 20px; - margin-left: 20px; + color: var(--text-inverse); + margin-right: var(--spacing-lg); + margin-left: var(--spacing-lg); + cursor: pointer; + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--border-radius-md); + transition: all var(--transition-normal); + background: rgba(255, 255, 255, 0.1); + min-height: 40px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.nav-items:hover { + background-color: rgba(255, 255, 255, 0.2); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* Add greeting text styling */ +.greeting-text { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + background: linear-gradient(45deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.7)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } .card-not-body { display: flex; @@ -117,7 +281,125 @@ } .navbar-button { - color: #262c6f; - border: none; - font-weight: bold; + color: var(--text-inverse); + border: 2px solid rgba(255, 255, 255, 0.6); + font-weight: var(--font-weight-semibold); + background: rgba(255, 255, 255, 0.15); + backdrop-filter: blur(20px); + border-radius: var(--border-radius-lg); + padding: 6px var(--spacing-md); + transition: all var(--transition-normal); + font-size: var(--font-size-sm); + min-width: 70px; + text-align: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; + height: 32px; + display: flex; + align-items: center; + justify-content: center; +} + +.navbar-button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left var(--transition-normal); +} + +.navbar-button:hover::before { + left: 100%; +} + +.navbar-button:hover { + background: rgba(255, 255, 255, 0.25); + border-color: rgba(255, 255, 255, 0.8); + transform: scale(1.02); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + color: var(--text-inverse); +} + +.navbar-button:active { + transform: scale(0.98); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } + +.navbar-button:focus { + outline: 2px solid rgba(255, 255, 255, 0.7); + outline-offset: 2px; +} + +/* Specific styling for Login button */ +.navbar-button:first-of-type { + background: rgba(255, 255, 255, 0.2); + border-color: rgba(255, 255, 255, 0.7); +} + +.navbar-button:first-of-type:hover { + background: rgba(255, 255, 255, 0.3); + border-color: rgba(255, 255, 255, 0.9); +} + +/* Specific styling for Signup button */ +.navbar-button:last-of-type { + background: linear-gradient(45deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.2)); + border: 2px solid var(--text-inverse); + font-weight: var(--font-weight-bold); +} + +.navbar-button:last-of-type:hover { + background: linear-gradient(45deg, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.3)); + border-color: var(--text-inverse); + box-shadow: 0 8px 20px rgba(255, 255, 255, 0.1); +} + +/* Enhanced Dropdown Menu Styling */ +.ant-dropdown-menu { + background: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(20px) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + border-radius: var(--border-radius-lg) !important; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15) !important; + padding: var(--spacing-sm) !important; + min-width: 200px !important; +} + +.ant-dropdown-menu-item { + padding: var(--spacing-sm) var(--spacing-md) !important; + margin: 2px 0 !important; + border-radius: var(--border-radius-md) !important; + transition: all var(--transition-normal) !important; + display: flex !important; + align-items: center !important; + gap: var(--spacing-sm) !important; + font-weight: var(--font-weight-medium) !important; + min-height: 40px !important; + cursor: pointer !important; + color: var(--text-primary) !important; +} + +.ant-dropdown-menu-item:hover { + background: linear-gradient(135deg, rgba(139, 92, 246, 0.1), rgba(59, 130, 246, 0.05)) !important; + color: var(--primary-color) !important; + transform: translateX(2px) !important; + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.15) !important; +} + +.ant-dropdown-menu-item:active { + transform: translateX(2px) !important; + box-shadow: 0 2px 6px rgba(139, 92, 246, 0.1) !important; +} + +.ant-dropdown-menu-item .anticon { + font-size: 16px !important; + color: inherit !important; + opacity: 0.8 !important; + transition: all var(--transition-normal) !important; +} + + diff --git a/services/web/src/components/navBar/navBar.tsx b/services/web/src/components/navBar/navBar.tsx index 39b95e46..6cf8db43 100644 --- a/services/web/src/components/navBar/navBar.tsx +++ b/services/web/src/components/navBar/navBar.tsx @@ -122,18 +122,24 @@ const Navbar: React.FC = (props) => { {isLoggedIn ? ( -
{`Good Morning, ${name}!`}
-
- navigate("/my-profile")} - /> -
- -
- +
{`Good Morning, ${name}!`}
+ +
+
+ +
+
+ {name} + +
diff --git a/services/web/src/components/pastOrders/pastOrders.tsx b/services/web/src/components/pastOrders/pastOrders.tsx index 1db161f1..3074ddef 100644 --- a/services/web/src/components/pastOrders/pastOrders.tsx +++ b/services/web/src/components/pastOrders/pastOrders.tsx @@ -66,40 +66,39 @@ const PastOrders: React.FC = (props) => { const { pastOrders } = props; const renderAvatar = (url: string) => ( - + ); const renderOrderDescription = (order: Order) => ( - <> - navigate(`/orders?order_id=${order.id}`)} - > - {" "} - Order Details - , - - , - ]} - /> - +
+
{order.product.name}
+
+ ${(Number(order.product.price) * order.quantity).toFixed(2)} +
+
{formatDateFromIso(order.created_on)}
+
+ + +
+
); return ( @@ -110,12 +109,13 @@ const PastOrders: React.FC = (props) => { onBack={() => navigate("/shop")} /> - + {pastOrders.map((order) => ( - + @@ -123,26 +123,30 @@ const PastOrders: React.FC = (props) => { ))} - - + + + + + + diff --git a/services/web/src/components/pastOrders/styles.css b/services/web/src/components/pastOrders/styles.css index 5bedc604..aa433fdb 100644 --- a/services/web/src/components/pastOrders/styles.css +++ b/services/web/src/components/pastOrders/styles.css @@ -1,21 +1,286 @@ -.order-desc { - padding: 0 2%; +/* Modern Past Orders Page Styling */ +.page-header { + padding-left: 0; + padding-right: 0; + background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); + border-radius: var(--border-radius-lg); + margin-bottom: var(--spacing-lg); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); } -.order-desc .ant-descriptions-item-label, -.order-desc .ant-descriptions-item-content { - font-size: 18px; +/* Order Cards */ +.order-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: var(--border-radius-xl); + box-shadow: var(--shadow-elevation-medium); + transition: all 0.3s ease; + overflow: hidden; + position: relative; + margin-bottom: var(--spacing-lg); } -.return-btn { - width: 30%; - margin: 0 35%; +.order-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #8b5cf6, #a855f7, #c084fc); + opacity: 0; + transition: opacity 0.3s ease; } -.order-card { - padding-top: 24px; +.order-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-elevation-high); + border-color: rgba(139, 92, 246, 0.3); +} + +.order-card:hover::before { + opacity: 1; +} + +.order-card .ant-card-cover { + padding: var(--spacing-lg); + background: linear-gradient(135deg, rgba(139, 92, 246, 0.05) 0%, rgba(168, 85, 247, 0.05) 100%); + display: flex; + align-items: center; + justify-content: center; + min-height: 280px; } +.order-card .ant-card-body { + padding: var(--spacing-lg); + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(5px); +} + +/* Order Images */ .order-avatar { margin: auto; + border-radius: var(--border-radius-lg) !important; + box-shadow: var(--shadow-elevation-medium); + transition: all 0.3s ease; + border: 3px solid rgba(255, 255, 255, 0.8); + max-width: 100%; + height: auto; + object-fit: cover; +} + +.order-card:hover .order-avatar { + transform: scale(1.05); + box-shadow: var(--shadow-elevation-high); +} + +/* Order Information */ +.order-info { + text-align: center; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.order-product-name { + font-size: 16px; + font-weight: 700; + color: #1f2937; + line-height: 1.3; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + letter-spacing: -0.025em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + display: inline-block; +} + +.order-price { + font-size: 18px; + font-weight: 800; + color: #8b5cf6; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + letter-spacing: -0.025em; + text-shadow: 0 1px 2px rgba(139, 92, 246, 0.2); +} + +.order-date { + font-size: 12px; + font-weight: 500; + color: #6b7280; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + margin-bottom: var(--spacing-xs); +} + +/* Order Action Buttons */ +.order-actions { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + margin-top: var(--spacing-md); +} + +.order-card .ant-btn { + height: 36px; + padding: 0 var(--spacing-md); + background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%); + border: none; + border-radius: var(--border-radius-lg); + font-weight: 600; + font-size: 13px; + box-shadow: var(--shadow-elevation-medium); + transition: all 0.3s ease; + position: relative; + overflow: hidden; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + letter-spacing: 0.025em; + min-width: 120px; +} + +.order-card .ant-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s; +} + +.order-card .ant-btn:hover::before { + left: 100%; +} + +.order-card .ant-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: var(--shadow-elevation-high); + background: linear-gradient(135deg, #7c3aed 0%, #9333ea 100%); +} + +.order-card .ant-btn:active { + transform: translateY(0); +} + +.order-card .ant-btn:disabled { + background: rgba(156, 163, 175, 0.3) !important; + color: #9ca3af !important; + cursor: not-allowed; + transform: none !important; +} + +.order-card .ant-btn[key="return-order"]:disabled { + background: linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(5, 150, 105, 0.2) 100%) !important; + color: #10b981 !important; + font-weight: 700 !important; + border: 1px solid rgba(16, 185, 129, 0.3) !important; +} + +.order-card .ant-btn .anticon { + margin-right: var(--spacing-xs); +} + +/* Pagination */ +.pagination { + margin-top: var(--spacing-xl); + gap: var(--spacing-lg); +} + +.pagination .ant-btn { + height: 48px; + padding: 0 var(--spacing-xl); + background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(168, 85, 247, 0.1) 100%); + backdrop-filter: blur(10px); + border: 1px solid rgba(139, 92, 246, 0.3); + border-radius: var(--border-radius-lg); + font-weight: 600; + transition: all 0.3s ease; + box-shadow: var(--shadow-elevation-low); +} + +.pagination .ant-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%); + color: white; + transform: translateY(-2px); + box-shadow: var(--shadow-elevation-medium); +} + +.pagination .ant-btn:disabled { + background: rgba(156, 163, 175, 0.1); + border-color: rgba(156, 163, 175, 0.2); + color: #9ca3af; +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .order-card .ant-card-cover { + min-height: 240px; + } +} + +@media (max-width: 768px) { + .order-card { + margin-bottom: var(--spacing-md); + } + + .order-card .ant-card-cover { + min-height: 200px; + padding: var(--spacing-md); + } + + .order-card .ant-card-body { + padding: var(--spacing-md); + } + + .order-product-name { + font-size: 14px; + } + + .order-price { + font-size: 16px; + } + + .order-date { + font-size: 11px; + } + + .order-card .ant-btn { + height: 32px; + font-size: 12px; + min-width: 100px; + padding: 0 var(--spacing-sm); + } + + .pagination { + flex-direction: column; + align-items: center; + gap: var(--spacing-md); + } + + .pagination .ant-btn { + width: 200px; + } +} + +@media (max-width: 576px) { + .page-header .ant-page-header-heading-extra { + flex-direction: column; + gap: var(--spacing-sm); + margin-left: 10px; + margin-right: 10px; + } + + .page-header .ant-btn { + width: 100%; + } + + .order-actions { + flex-direction: row; + justify-content: center; + flex-wrap: wrap; + } } diff --git a/services/web/src/components/post/post.tsx b/services/web/src/components/post/post.tsx index b77bad96..726c9281 100644 --- a/services/web/src/components/post/post.tsx +++ b/services/web/src/components/post/post.tsx @@ -81,108 +81,154 @@ const Posts: React.FC = ({ errorMessage, }) => { return ( - - - - - - - - - - - - - {post && - post.content.split("\n").map((para) => ( - - {para} - - ))} - - - + +
+ + +
+ +
+ + +
+

{post && post.title}

+
+ {post && + `${post.author.nickname}, ${formatDateFromIso(post.CreatedAt)}`} +
+
+ +
+
+ +
+ + {post && + post.content.split("\n").map((para, index) => ( + + {para} + + ))} + +
+ + + +
+
+

Comments

, - ]} - /> - {post && - post.comments && - post.comments.map((comment) => ( - - - - - - - - - - - {comment.content.split("\n").map((para) => ( - {para} - ))} - - - - ))} - + +
+ +
+ {post && + post.comments && + post.comments.map((comment, index) => ( +
+ + +
+ +
+ + +
+
+ {`${comment.author.nickname}, ${formatDateFromIso( + comment.CreatedAt, + )}`} +
+ + {comment.content + .split("\n") + .map((para, paraIndex) => ( + + {para} + + ))} + +
+ +
+
+ ))} +
+
💬 New Comment} + open={isCommentFormOpen} footer={null} onCancel={() => setIsCommentFormOpen(false)} + className="comment-modal" + centered >
- + - + {hasErrored &&
{errorMessage}
} - +
+ + +
diff --git a/services/web/src/components/post/style.css b/services/web/src/components/post/style.css index 74f55686..aa0b0246 100644 --- a/services/web/src/components/post/style.css +++ b/services/web/src/components/post/style.css @@ -1,19 +1,516 @@ -.post-header { - padding-bottom: 0; +/* ===== POST PAGE MODERN DESIGN ===== */ + +/* Page Container */ +.post-page { + min-height: 100vh; + background: linear-gradient( + 135deg, + rgba(167, 139, 250, 0.1) 0%, + rgba(236, 72, 153, 0.1) 50%, + rgba(59, 130, 246, 0.1) 100% + ); + padding: var(--spacing-xl) var(--spacing-md); +} + +/* Main Post Card */ +.post-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 24px; + box-shadow: + 0 20px 40px rgba(0, 0, 0, 0.1), + 0 10px 20px rgba(0, 0, 0, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.8); + padding: var(--spacing-xl) calc(var(--spacing-xl) * 1.5); + max-width: 1200px; + width: 95%; + margin: 0 auto; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.post-card:hover { + transform: translateY(-2px); + box-shadow: + 0 25px 50px rgba(0, 0, 0, 0.15), + 0 15px 30px rgba(0, 0, 0, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.9); +} + +/* Post Header Section */ +.post-header-section { + margin-bottom: calc(var(--spacing-xl) * 1.5); + padding-bottom: var(--spacing-lg); + border-bottom: 1px solid rgba(229, 231, 235, 0.3); +} + +.post-avatar-container { + display: flex; + justify-content: center; + align-items: center; + padding: 4px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 50%; + transition: all 0.3s ease; +} + +.post-avatar-container:hover { + transform: scale(1.05); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); +} + +.post-avatar { + border: 3px solid rgba(255, 255, 255, 0.9) !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; +} + +.post-meta { + padding-left: var(--spacing-md); +} + +.post-title { + font-size: 28px !important; + font-weight: 800 !important; + color: #1f2937 !important; + margin: 0 0 var(--spacing-sm) 0 !important; + line-height: 1.3 !important; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + letter-spacing: -0.025em; + background: linear-gradient(135deg, #1f2937 0%, #4b5563 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } .post-subtitle { - padding: 0; + font-size: 16px; + font-weight: 600; + color: #6b7280; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + margin: 0; } -.comment-heading { - padding-top: 0; +/* Post Content Section */ +.post-content-section { + margin-bottom: calc(var(--spacing-xl) * 1.5); + max-width: 900px; } -.comment-title { - padding: 0; +.post-content { + margin: 0; } -.comment-row { +.post-paragraph { + font-size: 17px !important; + line-height: 1.8 !important; + color: #374151 !important; + margin-bottom: calc(var(--spacing-md) * 1.2) !important; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + text-align: justify; +} + +.post-paragraph:last-child { + margin-bottom: 0 !important; +} + +/* Post Divider */ +.post-divider { + background: linear-gradient( + 90deg, + transparent 0%, + rgba(156, 163, 175, 0.3) 20%, + rgba(156, 163, 175, 0.6) 50%, + rgba(156, 163, 175, 0.3) 80%, + transparent 100% + ); + height: 2px; + border: none; + margin: var(--spacing-xl) 0; +} + +/* Comments Section */ +.comments-section { + margin-top: calc(var(--spacing-xl) * 1.5); + max-width: 100%; +} + +.comments-header { + display: flex; + justify-content: space-between; align-items: center; + margin-bottom: var(--spacing-xl); + flex-wrap: wrap; + gap: var(--spacing-md); +} + +.comments-title { + font-size: 24px !important; + font-weight: 700 !important; + color: #1f2937 !important; + margin: 0 !important; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + letter-spacing: -0.025em; +} + +.add-comment-btn { + background: linear-gradient(135deg, #8b5cf6 0%, #06b6d4 100%) !important; + border: none !important; + height: 44px !important; + padding: 0 var(--spacing-lg) !important; + font-weight: 600 !important; + font-size: 14px !important; + letter-spacing: 0.5px !important; + box-shadow: + 0 4px 12px rgba(139, 92, 246, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2) !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +.add-comment-btn:hover, +.add-comment-btn:focus { + transform: translateY(-2px) !important; + box-shadow: + 0 6px 20px rgba(139, 92, 246, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.3) !important; + background: linear-gradient(135deg, #7c3aed 0%, #0891b2 100%) !important; +} + +.add-comment-btn:active { + transform: translateY(0) !important; +} + +/* Comments List */ +.comments-list { + display: flex; + flex-direction: column; + gap: var(--spacing-lg); +} + +.comment-item { + background: rgba(248, 250, 252, 0.8); + backdrop-filter: blur(10px); + border: 1px solid rgba(226, 232, 240, 0.6); + border-radius: 16px; + padding: var(--spacing-lg) calc(var(--spacing-lg) * 1.5); + transition: all 0.3s ease; + width: 100%; +} + +.comment-item .ant-row { + width: 100% !important; + align-items: flex-start !important; + justify-content: flex-start !important; +} + +.comment-item .ant-col { + text-align: left !important; +} + +.comment-item .ant-col[flex="auto"] { + flex: 1 1 auto !important; + max-width: calc(100% - 70px) !important; + width: calc(100% - 70px) !important; +} + +.comment-item:hover { + background: rgba(255, 255, 255, 0.9); + transform: translateX(2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); +} + +.comment-avatar-container { + display: flex; + justify-content: center; + align-items: center; + padding: 2px; + background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%); + border-radius: 50%; + transition: all 0.3s ease; +} + +.comment-avatar-container:hover { + transform: scale(1.05); +} + +.comment-avatar { + border: 2px solid rgba(255, 255, 255, 0.9) !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; +} + +.comment-content { + padding-left: var(--spacing-md); + flex: 1 1 auto !important; + min-width: 0; + width: 100% !important; + max-width: 100% !important; + display: flex !important; + flex-direction: column !important; + align-items: flex-start !important; + justify-content: flex-start !important; +} + +.comment-meta { + font-size: 15px; + font-weight: 600; + color: #6b7280; + margin-bottom: var(--spacing-md); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + align-self: flex-start !important; + width: 100% !important; + max-width: 100% !important; + display: block !important; + text-align: left !important; +} + +.comment-text { + margin: 0 !important; + width: 100% !important; + max-width: 100% !important; + align-self: flex-start !important; + text-align: left !important; + display: block !important; + flex: 1 1 auto !important; +} + +.comment-text .ant-typography { + margin: 0 !important; + text-align: left !important; + width: 100% !important; + max-width: 100% !important; + display: block !important; +} + +.comment-paragraph { + font-size: 16px !important; + line-height: 1.7 !important; + color: #4b5563 !important; + margin-bottom: var(--spacing-sm) !important; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + text-align: left !important; + width: 100% !important; + display: block !important; +} + +.comment-paragraph p { + text-align: left !important; + margin: 0 !important; + width: 100% !important; +} + +.comment-paragraph:last-child { + margin-bottom: 0 !important; +} + +/* Modal Styling */ +.comment-modal .ant-modal-content { + background: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(20px) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + border-radius: 20px !important; + box-shadow: + 0 25px 50px rgba(0, 0, 0, 0.15), + 0 15px 30px rgba(0, 0, 0, 0.08) !important; + overflow: hidden !important; +} + +.comment-modal .ant-modal-header { + background: transparent !important; + border-bottom: 1px solid rgba(229, 231, 235, 0.6) !important; + padding: var(--spacing-lg) var(--spacing-xl) !important; +} + +.comment-modal .ant-modal-body { + padding: var(--spacing-xl) !important; +} + +.modal-title { + font-size: 20px !important; + font-weight: 700 !important; + color: #1f2937 !important; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.comment-form .ant-form-item-label > label { + font-weight: 600 !important; + color: #374151 !important; + font-size: 14px !important; +} + +.comment-textarea { + border: 2px solid rgba(229, 231, 235, 0.8) !important; + border-radius: 12px !important; + padding: var(--spacing-md) !important; + font-size: 15px !important; + line-height: 1.6 !important; + transition: all 0.3s ease !important; + background: rgba(249, 250, 251, 0.8) !important; +} + +.comment-textarea:focus { + border-color: #8b5cf6 !important; + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1) !important; + background: rgba(255, 255, 255, 0.9) !important; +} + +.comment-textarea::placeholder { + color: #9ca3af !important; + font-style: italic; +} + +.form-actions { + margin-bottom: 0 !important; +} + +.modal-buttons { + display: flex; + gap: var(--spacing-md); + justify-content: flex-end; + margin-top: var(--spacing-lg); +} + +.cancel-btn { + height: 40px !important; + padding: 0 var(--spacing-lg) !important; + font-weight: 600 !important; + border: 2px solid rgba(229, 231, 235, 0.8) !important; + background: rgba(249, 250, 251, 0.8) !important; + color: #6b7280 !important; + border-radius: 10px !important; + transition: all 0.3s ease !important; +} + +.cancel-btn:hover { + border-color: #d1d5db !important; + background: rgba(243, 244, 246, 0.9) !important; + color: #4b5563 !important; + transform: translateY(-1px) !important; +} + +.submit-btn { + height: 40px !important; + padding: 0 var(--spacing-lg) !important; + font-weight: 600 !important; + background: linear-gradient(135deg, #8b5cf6 0%, #06b6d4 100%) !important; + border: none !important; + border-radius: 10px !important; + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3) !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +.submit-btn:hover, +.submit-btn:focus { + background: linear-gradient(135deg, #7c3aed 0%, #0891b2 100%) !important; + transform: translateY(-2px) !important; + box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4) !important; +} + +.submit-btn:active { + transform: translateY(0) !important; +} + +.error-message { + color: #dc2626; + font-size: 14px; + font-weight: 500; + margin-bottom: var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-md); + background: rgba(254, 226, 226, 0.8); + border: 1px solid rgba(248, 113, 113, 0.3); + border-radius: 8px; +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .post-card { + max-width: 95%; + padding: var(--spacing-xl); + } + + .post-content-section { + max-width: 100%; + } + + .comments-section { + max-width: 100%; + } +} + +@media (max-width: 768px) { + .post-page { + padding: var(--spacing-lg) var(--spacing-sm); + } + + .post-card { + padding: var(--spacing-lg); + border-radius: 16px; + width: 98%; + } + + .post-title { + font-size: 24px !important; + } + + .comments-header { + flex-direction: column; + align-items: stretch; + } + + .add-comment-btn { + width: 100%; + justify-content: center; + } + + .modal-buttons { + flex-direction: column; + } + + .cancel-btn, + .submit-btn { + width: 100%; + justify-content: center; + } + + .comment-item { + padding: var(--spacing-md) var(--spacing-lg); + } + + .comment-content { + padding-left: var(--spacing-sm); + } + + .post-meta { + padding-left: 0; + margin-top: var(--spacing-md); + } +} + +@media (max-width: 480px) { + .post-card { + margin: 0; + border-radius: 12px; + width: 100%; + padding: var(--spacing-md); + } + + .post-title { + font-size: 20px !important; + } + + .comments-title { + font-size: 20px !important; + } + + .post-paragraph { + font-size: 16px !important; + text-align: left; + } + + .comment-paragraph { + font-size: 15px !important; + text-align: left; + } + + .comment-item { + padding: var(--spacing-sm) var(--spacing-md); + } } diff --git a/services/web/src/components/profile/profile.css b/services/web/src/components/profile/profile.css index 28f6d083..35e537b9 100644 --- a/services/web/src/components/profile/profile.css +++ b/services/web/src/components/profile/profile.css @@ -1,44 +1,521 @@ -.avatar-uploader { - width: auto; - margin-left: 10px; -} +/* ===== PROFILE PAGE MODERN DESIGN ===== */ -.page-container { - width: 65%; - min-width: 900px; - margin: auto; +/* Page Container */ +.profile-page { + min-height: 100vh; + background: linear-gradient( + 135deg, + rgba(167, 139, 250, 0.1) 0%, + rgba(236, 72, 153, 0.1) 50%, + rgba(59, 130, 246, 0.1) 100% + ); + padding: var(--spacing-xl) var(--spacing-lg); } -.profile-header { - padding-left: 0; +.page-container.profile-page { + max-width: 1400px; + width: 90%; + margin: 0 auto; + min-width: auto; } -.change-email-btn { - margin-left: 50px; +/* Profile Header Section */ +.profile-header-section { + text-align: center; + margin-bottom: calc(var(--spacing-xl) * 1.5); + padding-bottom: var(--spacing-xl); + border-bottom: 1px solid rgba(255, 255, 255, 0.2); } -.change-phone-number-btn { - margin-left: 50px; +.profile-title { + font-size: 36px !important; + font-weight: 800 !important; + color: #1f2937 !important; + margin: 0 0 var(--spacing-sm) 0 !important; + line-height: 1.2 !important; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + letter-spacing: -0.025em; + background: linear-gradient(135deg, #1f2937 0%, #4b5563 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } -.profile-video { - width: 60%; +.profile-subtitle { + font-size: 18px; + color: #6b7280; + font-weight: 500; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } -.more-icon { - color: black; - font-size: x-large; +/* Content Wrapper */ +.profile-content-wrapper { + display: flex; + flex-direction: column; + gap: var(--spacing-xl); } -.upload-video-button { +/* Cards */ +.profile-card, +.video-section-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 24px; + box-shadow: + 0 20px 40px rgba(0, 0, 0, 0.1), + 0 10px 20px rgba(0, 0, 0, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.8); + padding: calc(var(--spacing-xl) * 1.5) calc(var(--spacing-xl) * 2); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); width: 100%; - height: 100%; - min-width: 900px; +} + +.profile-card:hover, +.video-section-card:hover { + transform: translateY(-2px); + box-shadow: + 0 25px 50px rgba(0, 0, 0, 0.15), + 0 15px 30px rgba(0, 0, 0, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.9); +} + +/* Profile Avatar Section */ +.profile-avatar-section { + display: flex; + justify-content: center; align-items: center; + position: relative; +} + +.avatar-container { + position: relative; + display: flex; justify-content: center; + align-items: center; + padding: 8px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 50%; + transition: all 0.3s ease; +} + +.avatar-container:hover { + transform: scale(1.05); + box-shadow: 0 12px 30px rgba(102, 126, 234, 0.3); +} + +.profile-avatar { + border: 4px solid rgba(255, 255, 255, 0.9) !important; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important; + transition: all 0.3s ease !important; +} + +.change-pic-btn { + background: linear-gradient(135deg, #8b5cf6 0%, #06b6d4 100%) !important; + border: none !important; + box-shadow: + 0 6px 20px rgba(139, 92, 246, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.2) !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + width: 48px !important; + height: 48px !important; +} + +.change-pic-btn:hover, +.change-pic-btn:focus { + transform: translateY(-2px) scale(1.05) !important; + box-shadow: + 0 8px 25px rgba(139, 92, 246, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.3) !important; + background: linear-gradient(135deg, #7c3aed 0%, #0891b2 100%) !important; +} + +/* Profile Information */ +.profile-info { display: flex; - margin-top: 40px; + flex-direction: column; + gap: calc(var(--spacing-lg) * 1.5); + width: 100%; +} + +.info-item { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + padding: calc(var(--spacing-lg) * 1.2) var(--spacing-xl); + background: rgba(248, 250, 252, 0.8); + backdrop-filter: blur(10px); + border: 1px solid rgba(226, 232, 240, 0.6); + border-radius: 16px; + transition: all 0.3s ease; + width: 100%; +} + +.info-item:hover { + background: rgba(255, 255, 255, 0.9); + transform: translateX(2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08); +} + +.info-label { + font-size: 14px; + font-weight: 700; + color: #6b7280; + text-transform: uppercase; + letter-spacing: 0.5px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.info-value { + font-size: 20px; + font-weight: 600; + color: #1f2937; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + line-height: 1.4; + flex: 1; + min-width: 0; +} + +.info-value-with-action { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: var(--spacing-lg); + width: 100%; +} + +/* Action Buttons */ +.action-btn { + background: linear-gradient(135deg, #8b5cf6 0%, #06b6d4 100%) !important; + border: none !important; + height: 40px !important; + padding: 0 var(--spacing-lg) !important; + font-weight: 600 !important; + font-size: 14px !important; + letter-spacing: 0.5px !important; + box-shadow: + 0 4px 12px rgba(139, 92, 246, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2) !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + flex-shrink: 0; +} + +.action-btn:hover, +.action-btn:focus { + transform: translateY(-2px) !important; + box-shadow: + 0 6px 20px rgba(139, 92, 246, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.3) !important; + background: linear-gradient(135deg, #7c3aed 0%, #0891b2 100%) !important; +} + +.action-btn:active { + transform: translateY(0) !important; +} + +/* Video Section */ +.video-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: var(--spacing-xl); + flex-wrap: wrap; + gap: var(--spacing-lg); +} + +.video-info { + flex: 1; + min-width: 0; +} + +.video-title { + font-size: 28px !important; + font-weight: 700 !important; + color: #1f2937 !important; + margin: 0 0 var(--spacing-sm) 0 !important; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + letter-spacing: -0.025em; +} + +.video-subtitle { + font-size: 16px; + color: #6b7280; + font-weight: 500; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.upload-video-btn, +.video-dropdown-btn { + background: linear-gradient(135deg, #8b5cf6 0%, #06b6d4 100%) !important; + border: none !important; + height: 48px !important; + padding: 0 var(--spacing-xl) !important; + font-weight: 600 !important; + font-size: 16px !important; + letter-spacing: 0.5px !important; + border-radius: 24px !important; + box-shadow: + 0 6px 20px rgba(139, 92, 246, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.2) !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +.video-dropdown-btn .ant-btn { + border-radius: 24px 0 0 24px !important; +} + +.video-dropdown-btn .ant-dropdown-trigger { + border-radius: 0 24px 24px 0 !important; +} + +.upload-video-btn:hover, +.upload-video-btn:focus, +.video-dropdown-btn:hover, +.video-dropdown-btn:focus { + transform: translateY(-2px) !important; + box-shadow: + 0 8px 25px rgba(139, 92, 246, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.3) !important; + background: linear-gradient(135deg, #7c3aed 0%, #0891b2 100%) !important; } -.button { + +/* Video Player */ +.video-player-container { + margin-top: var(--spacing-xl); + padding: calc(var(--spacing-lg) * 1.5); + background: rgba(0, 0, 0, 0.05); + border-radius: 16px; + border: 1px solid rgba(0, 0, 0, 0.08); + width: 100%; +} + +.video-container { + display: flex; + justify-content: center; align-items: center; + width: 100%; +} + +.profile-video { + width: 100%; + max-width: 1000px; + height: auto; + border-radius: 12px; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + transition: all 0.3s ease; +} + +.profile-video:hover { + transform: scale(1.02); + box-shadow: 0 12px 35px rgba(0, 0, 0, 0.2); +} + +/* Modal Styling */ +.video-name-modal .ant-modal-content { + background: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(20px) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + border-radius: 20px !important; + box-shadow: + 0 25px 50px rgba(0, 0, 0, 0.15), + 0 15px 30px rgba(0, 0, 0, 0.08) !important; +} + +.video-name-modal .ant-modal-header { + background: transparent !important; + border-bottom: 1px solid rgba(229, 231, 235, 0.6) !important; + padding: var(--spacing-lg) var(--spacing-xl) !important; +} + +.video-name-modal .ant-modal-body { + padding: var(--spacing-xl) !important; +} + +.modal-title { + font-size: 20px !important; + font-weight: 700 !important; + color: #1f2937 !important; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.video-name-form .ant-form-item-label > label { + font-weight: 600 !important; + color: #374151 !important; + font-size: 14px !important; +} + +.video-name-input { + border: 2px solid rgba(229, 231, 235, 0.8) !important; + border-radius: 12px !important; + padding: var(--spacing-md) !important; + font-size: 16px !important; + transition: all 0.3s ease !important; + background: rgba(249, 250, 251, 0.8) !important; +} + +.video-name-input:focus { + border-color: #8b5cf6 !important; + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1) !important; + background: rgba(255, 255, 255, 0.9) !important; +} + +.modal-buttons { + display: flex; + gap: var(--spacing-md); + justify-content: flex-end; + margin-top: var(--spacing-lg); +} + +.cancel-btn { + height: 44px !important; + padding: 0 var(--spacing-lg) !important; + font-weight: 600 !important; + border: 2px solid rgba(229, 231, 235, 0.8) !important; + background: rgba(249, 250, 251, 0.8) !important; + color: #6b7280 !important; + border-radius: 12px !important; + transition: all 0.3s ease !important; +} + +.cancel-btn:hover { + border-color: #d1d5db !important; + background: rgba(243, 244, 246, 0.9) !important; + color: #4b5563 !important; + transform: translateY(-1px) !important; +} + +.submit-btn { + height: 44px !important; + padding: 0 var(--spacing-lg) !important; + font-weight: 600 !important; + background: linear-gradient(135deg, #8b5cf6 0%, #06b6d4 100%) !important; + border: none !important; + border-radius: 12px !important; + box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3) !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +.submit-btn:hover, +.submit-btn:focus { + background: linear-gradient(135deg, #7c3aed 0%, #0891b2 100%) !important; + transform: translateY(-2px) !important; + box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4) !important; +} + +.error-message { + color: #dc2626; + font-size: 14px; + font-weight: 500; + margin-bottom: var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-md); + background: rgba(254, 226, 226, 0.8); + border: 1px solid rgba(248, 113, 113, 0.3); + border-radius: 8px; +} + +/* Responsive Design */ +@media (max-width: 1440px) { + .page-container.profile-page { + width: 95%; + } +} + +@media (max-width: 1024px) { + .profile-page { + padding: var(--spacing-lg) var(--spacing-md); + } + + .page-container.profile-page { + width: 98%; + } + + .profile-card, + .video-section-card { + padding: var(--spacing-xl); + border-radius: 16px; + } + + .info-item { + padding: var(--spacing-lg); + } +} + +@media (max-width: 768px) { + .profile-title { + font-size: 28px !important; + } + + .video-title { + font-size: 24px !important; + } + + .page-container.profile-page { + width: 100%; + padding: var(--spacing-md); + } + + .profile-card, + .video-section-card { + padding: var(--spacing-lg); + margin: 0 -8px; + } + + .info-value-with-action { + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-md); + } + + .action-btn { + width: 100%; + justify-content: center; + } + + .video-header { + flex-direction: column; + align-items: stretch; + } + + .upload-video-btn, + .video-dropdown-btn { + width: 100%; + justify-content: center; + } + + .modal-buttons { + flex-direction: column; + } + + .cancel-btn, + .submit-btn { + width: 100%; + justify-content: center; + } + + .info-item { + padding: var(--spacing-md) var(--spacing-lg); + } +} + +@media (max-width: 480px) { + .profile-card, + .video-section-card { + padding: var(--spacing-lg); + border-radius: 12px; + } + + .profile-title { + font-size: 24px !important; + } + + .video-title { + font-size: 20px !important; + } + + .info-item { + padding: var(--spacing-md); + } } diff --git a/services/web/src/components/profile/profile.tsx b/services/web/src/components/profile/profile.tsx index f3a67ea9..5bd47d24 100644 --- a/services/web/src/components/profile/profile.tsx +++ b/services/web/src/components/profile/profile.tsx @@ -239,146 +239,199 @@ const Profile: React.FC = (props) => { const renderChangePicButton = () => ( - - - {userData.number} - - - - - +
+ + +
+ + +
+ +
+
+
+ + +
+
+
Name
+
{userData.name}
+
+ +
+
Email
+
+ {userData.email} + +
+
+ +
+
Phone Number
+
+ {userData.number} + +
+
+
+ +
+
); const renderVideo = () => ( - - - <> - - - - +
+ +
); return ( - - - - + +
+

Your Profile

+

+ Manage your personal information and settings +

+
+ + + - - - - ) : ( - - ), - ]} - /> - - {videoData && ( - - - - )} + + +
+
+

My Personal Video

+

Max File Size: 10MB

+
+
+ {videoData ? ( + + + Manage Video + + ) : ( + + )} +
+
+ + + + {videoData && ( +
{renderVideo()}
+ )} +
📹 Enter New Video Name} + open={isVideoModalOpen} footer={null} onCancel={() => setIsVideoModalOpen(false)} + className="video-name-modal" + centered >
- + - + {hasErrored &&
{errorMessage}
} - +
+ + +
diff --git a/services/web/src/components/shop/shop.tsx b/services/web/src/components/shop/shop.tsx index ed582d33..5155c574 100644 --- a/services/web/src/components/shop/shop.tsx +++ b/services/web/src/components/shop/shop.tsx @@ -74,8 +74,9 @@ const ProductDescription: React.FC<{ product: Product; onBuyProduct: (product: Product) => void; }> = ({ product, onBuyProduct }) => ( - <> - +
+
{product.name}
+
${Number(product.price).toFixed(2)}
- +
); const Shop: React.FC = (props) => { @@ -141,7 +142,7 @@ const Shop: React.FC = (props) => { - + {products.map((product) => (