This commit is contained in:
DESKTOP-RTLN3BA\$punk 2024-08-12 00:32:42 -07:00
parent 24f641347d
commit 63146aa9b7
86 changed files with 18766 additions and 0 deletions

36
README.md Normal file
View file

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

12
backend/.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
__pycache__
__pycache__/
.__pycache__

View file

@ -0,0 +1,830 @@
import asyncio
import json
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union, cast
from langchain_community.graphs.graph_document import GraphDocument, Node, Relationship
from langchain_core.documents import Document
from langchain_core.language_models import BaseLanguageModel
from langchain_core.messages import SystemMessage
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import (
ChatPromptTemplate,
HumanMessagePromptTemplate,
PromptTemplate,
)
from langchain_core.pydantic_v1 import BaseModel, Field, create_model
from langchain_core.runnables import RunnableConfig
examples = [
{
"text": (
"Adam is a software engineer in Microsoft since 2009, "
"and last year he got an award as the Best Talent"
),
"head": "Adam",
"head_type": "Person",
"relation": "WORKS_FOR",
"tail": "Microsoft",
"tail_type": "Company",
},
{
"text": (
"Adam is a software engineer in Microsoft since 2009, "
"and last year he got an award as the Best Talent"
),
"head": "Adam",
"head_type": "Person",
"relation": "HAS_AWARD",
"tail": "Best Talent",
"tail_type": "Award",
},
{
"text": (
"Microsoft is a tech company that provide "
"several products such as Microsoft Word"
),
"head": "Microsoft Word",
"head_type": "Product",
"relation": "PRODUCED_BY",
"tail": "Microsoft",
"tail_type": "Company",
},
{
"text": "Microsoft Word is a lightweight app that accessible offline",
"head": "Microsoft Word",
"head_type": "Product",
"relation": "HAS_CHARACTERISTIC",
"tail": "lightweight app",
"tail_type": "Characteristic",
},
{
"text": "Microsoft Word is a lightweight app that accessible offline",
"head": "Microsoft Word",
"head_type": "Product",
"relation": "HAS_CHARACTERISTIC",
"tail": "accessible offline",
"tail_type": "Characteristic",
},
]
system_prompt = (
"# Knowledge Graph Instructions for GPT-4\n"
"## 1. Overview\n"
"You are a top-tier algorithm designed for extracting information in structured "
"formats to build a knowledge graph.\n"
"Try to capture as much information from the text as possible without "
"sacrificing accuracy. Do not add any information that is not explicitly "
"mentioned in the text.\n"
"- **Nodes** represent entities and concepts.\n"
"- The aim is to achieve simplicity and clarity in the knowledge graph, making it\n"
"accessible for a vast audience.\n"
"## 2. Labeling Nodes\n"
"- **Consistency**: Ensure you use available types for node labels.\n"
"Ensure you use basic or elementary types for node labels.\n"
"- For example, when you identify an entity representing a person, "
"always label it as **'person'**. Avoid using more specific terms "
"like 'mathematician' or 'scientist'."
"- **Node IDs**: Never utilize integers as node IDs. Node IDs should be "
"names or human-readable identifiers found in the text.\n"
"- **Relationships** represent connections between entities or concepts.\n"
"Ensure consistency and generality in relationship types when constructing "
"knowledge graphs. Instead of using specific and momentary types "
"such as 'BECAME_PROFESSOR', use more general and timeless relationship types "
"like 'PROFESSOR'. Make sure to use general and timeless relationship types!\n"
"## 3. Coreference Resolution\n"
"- **Maintain Entity Consistency**: When extracting entities, it's vital to "
"ensure consistency.\n"
'If an entity, such as "John Doe", is mentioned multiple times in the text '
'but is referred to by different names or pronouns (e.g., "Joe", "he"),'
"always use the most complete identifier for that entity throughout the "
'knowledge graph. In this example, use "John Doe" as the entity ID.\n'
"Remember, the knowledge graph should be coherent and easily understandable, "
"so maintaining consistency in entity references is crucial.\n"
"## 4. Strict Compliance\n"
"Adhere to the rules strictly. Non-compliance will result in termination."
)
default_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
system_prompt,
),
(
"human",
(
"Note: The information given to you is information about a User's Web Browsing History."
"Tip: Make sure to answer in the correct format and do "
"not include any explanations. "
"Use the given format to extract information from the "
"following input: {input}"
),
),
]
)
def _get_additional_info(input_type: str) -> str:
# Check if the input_type is one of the allowed values
if input_type not in ["node", "relationship", "property"]:
raise ValueError("input_type must be 'node', 'relationship', or 'property'")
# Perform actions based on the input_type
if input_type == "node":
return (
"Ensure you use basic or elementary types for node labels.\n"
"For example, when you identify an entity representing a person, "
"always label it as **'Person'**. Avoid using more specific terms "
"like 'Mathematician' or 'Scientist'"
)
elif input_type == "relationship":
return (
"Instead of using specific and momentary types such as "
"'BECAME_PROFESSOR', use more general and timeless relationship types "
"like 'PROFESSOR'. However, do not sacrifice any accuracy for generality"
)
elif input_type == "property":
return ""
return ""
def optional_enum_field(
enum_values: Optional[List[str]] = None,
description: str = "",
input_type: str = "node",
llm_type: Optional[str] = None,
**field_kwargs: Any,
) -> Any:
"""Utility function to conditionally create a field with an enum constraint."""
# Only openai supports enum param
if enum_values and llm_type == "openai-chat":
return Field(
...,
enum=enum_values,
description=f"{description}. Available options are {enum_values}",
**field_kwargs,
)
elif enum_values:
return Field(
...,
description=f"{description}. Available options are {enum_values}",
**field_kwargs,
)
else:
additional_info = _get_additional_info(input_type)
return Field(..., description=description + additional_info, **field_kwargs)
class _Graph(BaseModel):
nodes: Optional[List]
relationships: Optional[List]
class UnstructuredRelation(BaseModel):
head: str = Field(
description=(
"extracted head entity like Microsoft, Apple, John. "
"Must use human-readable unique identifier."
)
)
head_type: str = Field(
description="type of the extracted head entity like Person, Company, etc"
)
relation: str = Field(description="relation between the head and the tail entities")
tail: str = Field(
description=(
"extracted tail entity like Microsoft, Apple, John. "
"Must use human-readable unique identifier."
)
)
tail_type: str = Field(
description="type of the extracted tail entity like Person, Company, etc"
)
def create_unstructured_prompt(
node_labels: Optional[List[str]] = None, rel_types: Optional[List[str]] = None
) -> ChatPromptTemplate:
node_labels_str = str(node_labels) if node_labels else ""
rel_types_str = str(rel_types) if rel_types else ""
base_string_parts = [
"You are a top-tier algorithm designed for extracting information in "
"structured formats to build a knowledge graph. Your task is to identify "
"the entities and relations requested with the user prompt from a given "
"text. You must generate the output in a JSON format containing a list "
'with JSON objects. Each object should have the keys: "head", '
'"head_type", "relation", "tail", and "tail_type". The "head" '
"key must contain the text of the extracted entity with one of the types "
"from the provided list in the user prompt.",
f'The "head_type" key must contain the type of the extracted head entity, '
f"which must be one of the types from {node_labels_str}."
if node_labels
else "",
f'The "relation" key must contain the type of relation between the "head" '
f'and the "tail", which must be one of the relations from {rel_types_str}.'
if rel_types
else "",
f'The "tail" key must represent the text of an extracted entity which is '
f'the tail of the relation, and the "tail_type" key must contain the type '
f"of the tail entity from {node_labels_str}."
if node_labels
else "",
"Attempt to extract as many entities and relations as you can. Maintain "
"Entity Consistency: When extracting entities, it's vital to ensure "
'consistency. If an entity, such as "John Doe", is mentioned multiple '
"times in the text but is referred to by different names or pronouns "
'(e.g., "Joe", "he"), always use the most complete identifier for '
"that entity. The knowledge graph should be coherent and easily "
"understandable, so maintaining consistency in entity references is "
"crucial.",
"IMPORTANT NOTES:\n- Don't add any explanation and text.",
]
system_prompt = "\n".join(filter(None, base_string_parts))
system_message = SystemMessage(content=system_prompt)
parser = JsonOutputParser(pydantic_object=UnstructuredRelation)
human_string_parts = [
"Based on the following example, extract entities and "
"relations from the provided text.\n\n",
"Use the following entity types, don't use other entity "
"that is not defined below:"
"# ENTITY TYPES:"
"{node_labels}"
if node_labels
else "",
"Use the following relation types, don't use other relation "
"that is not defined below:"
"# RELATION TYPES:"
"{rel_types}"
if rel_types
else "",
"Below are a number of examples of text and their extracted "
"entities and relationships."
"{examples}\n"
"For the following text, extract entities and relations as "
"in the provided example."
"{format_instructions}\nText: {input}",
]
human_prompt_string = "\n".join(filter(None, human_string_parts))
human_prompt = PromptTemplate(
template=human_prompt_string,
input_variables=["input"],
partial_variables={
"format_instructions": parser.get_format_instructions(),
"node_labels": node_labels,
"rel_types": rel_types,
"examples": examples,
},
)
human_message_prompt = HumanMessagePromptTemplate(prompt=human_prompt)
chat_prompt = ChatPromptTemplate.from_messages(
[system_message, human_message_prompt]
)
return chat_prompt
def create_simple_model(
node_labels: Optional[List[str]] = None,
rel_types: Optional[List[str]] = None,
node_properties: Union[bool, List[str]] = False,
llm_type: Optional[str] = None,
relationship_properties: Union[bool, List[str]] = False,
) -> Type[_Graph]:
"""
Create a simple graph model with optional constraints on node
and relationship types.
Args:
node_labels (Optional[List[str]]): Specifies the allowed node types.
Defaults to None, allowing all node types.
rel_types (Optional[List[str]]): Specifies the allowed relationship types.
Defaults to None, allowing all relationship types.
node_properties (Union[bool, List[str]]): Specifies if node properties should
be included. If a list is provided, only properties with keys in the list
will be included. If True, all properties are included. Defaults to False.
relationship_properties (Union[bool, List[str]]): Specifies if relationship
properties should be included. If a list is provided, only properties with
keys in the list will be included. If True, all properties are included.
Defaults to False.
llm_type (Optional[str]): The type of the language model. Defaults to None.
Only openai supports enum param: openai-chat.
Returns:
Type[_Graph]: A graph model with the specified constraints.
Raises:
ValueError: If 'id' is included in the node or relationship properties list.
"""
node_fields: Dict[str, Tuple[Any, Any]] = {
"id": (
str,
Field(..., description="Name or human-readable unique identifier."),
),
"type": (
str,
optional_enum_field(
node_labels,
description="The type or label of the node.",
input_type="node",
llm_type=llm_type,
),
),
}
if node_properties:
if isinstance(node_properties, list) and "id" in node_properties:
raise ValueError("The node property 'id' is reserved and cannot be used.")
# Map True to empty array
node_properties_mapped: List[str] = (
[] if node_properties is True else node_properties
)
class Property(BaseModel):
"""A single property consisting of key and value"""
key: str = optional_enum_field(
node_properties_mapped,
description="Property key.",
input_type="property",
llm_type=llm_type,
)
value: str = Field(..., description="value")
node_fields["properties"] = (
Optional[List[Property]],
Field(None, description="List of node properties"),
)
SimpleNode = create_model("SimpleNode", **node_fields) # type: ignore
relationship_fields: Dict[str, Tuple[Any, Any]] = {
"source_node_id": (
str,
Field(
...,
description="Name or human-readable unique identifier of source node",
),
),
"source_node_type": (
str,
optional_enum_field(
node_labels,
description="The type or label of the source node.",
input_type="node",
llm_type=llm_type,
),
),
"target_node_id": (
str,
Field(
...,
description="Name or human-readable unique identifier of target node",
),
),
"target_node_type": (
str,
optional_enum_field(
node_labels,
description="The type or label of the target node.",
input_type="node",
llm_type=llm_type,
),
),
"type": (
str,
optional_enum_field(
rel_types,
description="The type of the relationship.",
input_type="relationship",
llm_type=llm_type,
),
),
}
if relationship_properties:
if (
isinstance(relationship_properties, list)
and "id" in relationship_properties
):
raise ValueError(
"The relationship property 'id' is reserved and cannot be used."
)
# Map True to empty array
relationship_properties_mapped: List[str] = (
[] if relationship_properties is True else relationship_properties
)
class RelationshipProperty(BaseModel):
"""A single property consisting of key and value"""
key: str = optional_enum_field(
relationship_properties_mapped,
description="Property key.",
input_type="property",
llm_type=llm_type,
)
value: str = Field(..., description="value")
relationship_fields["properties"] = (
Optional[List[RelationshipProperty]],
Field(None, description="List of relationship properties"),
)
SimpleRelationship = create_model("SimpleRelationship", **relationship_fields) # type: ignore
class DynamicGraph(_Graph):
"""Represents a graph document consisting of nodes and relationships."""
nodes: Optional[List[SimpleNode]] = Field(description="List of nodes") # type: ignore
relationships: Optional[List[SimpleRelationship]] = Field( # type: ignore
description="List of relationships"
)
return DynamicGraph
def map_to_base_node(node: Any) -> Node:
"""Map the SimpleNode to the base Node."""
properties = {}
if hasattr(node, "properties") and node.properties:
for p in node.properties:
properties[format_property_key(p.key)] = p.value
return Node(id=node.id, type=node.type, properties=properties)
def map_to_base_relationship(rel: Any) -> Relationship:
"""Map the SimpleRelationship to the base Relationship."""
source = Node(id=rel.source_node_id, type=rel.source_node_type)
target = Node(id=rel.target_node_id, type=rel.target_node_type)
properties = {}
if hasattr(rel, "properties") and rel.properties:
for p in rel.properties:
properties[format_property_key(p.key)] = p.value
return Relationship(
source=source, target=target, type=rel.type, properties=properties
)
def _parse_and_clean_json(
argument_json: Dict[str, Any],
) -> Tuple[List[Node], List[Relationship]]:
nodes = []
for node in argument_json["nodes"]:
if not node.get("id"): # Id is mandatory, skip this node
continue
node_properties = {}
if "properties" in node and node["properties"]:
for p in node["properties"]:
node_properties[format_property_key(p["key"])] = p["value"]
nodes.append(
Node(
id=node["id"],
type=node.get("type"),
properties=node_properties,
)
)
relationships = []
for rel in argument_json["relationships"]:
# Mandatory props
if (
not rel.get("source_node_id")
or not rel.get("target_node_id")
or not rel.get("type")
):
continue
# Node type copying if needed from node list
if not rel.get("source_node_type"):
try:
rel["source_node_type"] = [
el.get("type")
for el in argument_json["nodes"]
if el["id"] == rel["source_node_id"]
][0]
except IndexError:
rel["source_node_type"] = None
if not rel.get("target_node_type"):
try:
rel["target_node_type"] = [
el.get("type")
for el in argument_json["nodes"]
if el["id"] == rel["target_node_id"]
][0]
except IndexError:
rel["target_node_type"] = None
rel_properties = {}
if "properties" in rel and rel["properties"]:
for p in rel["properties"]:
rel_properties[format_property_key(p["key"])] = p["value"]
source_node = Node(
id=rel["source_node_id"],
type=rel["source_node_type"],
)
target_node = Node(
id=rel["target_node_id"],
type=rel["target_node_type"],
)
relationships.append(
Relationship(
source=source_node,
target=target_node,
type=rel["type"],
properties=rel_properties,
)
)
return nodes, relationships
def _format_nodes(nodes: List[Node]) -> List[Node]:
return [
Node(
id=el.id.title() if isinstance(el.id, str) else el.id,
type=el.type.capitalize() # type: ignore[arg-type]
if el.type
else None, # handle empty strings # type: ignore[arg-type]
properties=el.properties,
)
for el in nodes
]
def _format_relationships(rels: List[Relationship]) -> List[Relationship]:
return [
Relationship(
source=_format_nodes([el.source])[0],
target=_format_nodes([el.target])[0],
type=el.type.replace(" ", "_").upper(),
properties=el.properties,
)
for el in rels
]
def format_property_key(s: str) -> str:
words = s.split()
if not words:
return s
first_word = words[0].lower()
capitalized_words = [word.capitalize() for word in words[1:]]
return "".join([first_word] + capitalized_words)
def _convert_to_graph_document(
raw_schema: Dict[Any, Any],
) -> Tuple[List[Node], List[Relationship]]:
# If there are validation errors
if not raw_schema["parsed"]:
try:
try: # OpenAI type response
argument_json = json.loads(
raw_schema["raw"].additional_kwargs["tool_calls"][0]["function"][
"arguments"
]
)
except Exception: # Google type response
argument_json = json.loads(
raw_schema["raw"].additional_kwargs["function_call"]["arguments"]
)
nodes, relationships = _parse_and_clean_json(argument_json)
except Exception: # If we can't parse JSON
return ([], [])
else: # If there are no validation errors use parsed pydantic object
parsed_schema: _Graph = raw_schema["parsed"]
nodes = (
[map_to_base_node(node) for node in parsed_schema.nodes if node.id]
if parsed_schema.nodes
else []
)
relationships = (
[
map_to_base_relationship(rel)
for rel in parsed_schema.relationships
if rel.type and rel.source_node_id and rel.target_node_id
]
if parsed_schema.relationships
else []
)
# Title / Capitalize
return _format_nodes(nodes), _format_relationships(relationships)
class LLMGraphTransformer:
"""Transform documents into graph-based documents using a LLM.
It allows specifying constraints on the types of nodes and relationships to include
in the output graph. The class supports extracting properties for both nodes and
relationships.
Args:
llm (BaseLanguageModel): An instance of a language model supporting structured
output.
allowed_nodes (List[str], optional): Specifies which node types are
allowed in the graph. Defaults to an empty list, allowing all node types.
allowed_relationships (List[str], optional): Specifies which relationship types
are allowed in the graph. Defaults to an empty list, allowing all relationship
types.
prompt (Optional[ChatPromptTemplate], optional): The prompt to pass to
the LLM with additional instructions.
strict_mode (bool, optional): Determines whether the transformer should apply
filtering to strictly adhere to `allowed_nodes` and `allowed_relationships`.
Defaults to True.
node_properties (Union[bool, List[str]]): If True, the LLM can extract any
node properties from text. Alternatively, a list of valid properties can
be provided for the LLM to extract, restricting extraction to those specified.
relationship_properties (Union[bool, List[str]]): If True, the LLM can extract
any relationship properties from text. Alternatively, a list of valid
properties can be provided for the LLM to extract, restricting extraction to
those specified.
Example:
.. code-block:: python
from langchain_experimental.graph_transformers import LLMGraphTransformer
from langchain_core.documents import Document
from langchain_openai import ChatOpenAI
llm=ChatOpenAI(temperature=0)
transformer = LLMGraphTransformer(
llm=llm,
allowed_nodes=["Person", "Organization"])
doc = Document(page_content="Elon Musk is suing OpenAI")
graph_documents = transformer.convert_to_graph_documents([doc])
"""
def __init__(
self,
llm: BaseLanguageModel,
allowed_nodes: List[str] = [],
allowed_relationships: List[str] = [],
prompt: Optional[ChatPromptTemplate] = None,
strict_mode: bool = True,
node_properties: Union[bool, List[str]] = False,
relationship_properties: Union[bool, List[str]] = False,
) -> None:
self.allowed_nodes = allowed_nodes
self.allowed_relationships = allowed_relationships
self.strict_mode = strict_mode
self._function_call = True
# Check if the LLM really supports structured output
try:
llm.with_structured_output(_Graph)
except NotImplementedError:
self._function_call = False
if not self._function_call:
if node_properties or relationship_properties:
raise ValueError(
"The 'node_properties' and 'relationship_properties' parameters "
"cannot be used in combination with a LLM that doesn't support "
"native function calling."
)
try:
import json_repair # type: ignore
self.json_repair = json_repair
except ImportError:
raise ImportError(
"Could not import json_repair python package. "
"Please install it with `pip install json-repair`."
)
prompt = prompt or create_unstructured_prompt(
allowed_nodes, allowed_relationships
)
self.chain = prompt | llm
else:
# Define chain
try:
llm_type = llm._llm_type # type: ignore
except AttributeError:
llm_type = None
schema = create_simple_model(
allowed_nodes,
allowed_relationships,
node_properties,
llm_type,
relationship_properties,
)
structured_llm = llm.with_structured_output(schema, include_raw=True)
prompt = prompt or default_prompt
self.chain = prompt | structured_llm
def process_response(
self, document: Document, config: Optional[RunnableConfig] = None
) -> GraphDocument:
"""
Processes a single document, transforming it into a graph document using
an LLM based on the model's schema and constraints.
"""
text = document.page_content
raw_schema = self.chain.invoke({"input": text}, config=config)
if self._function_call:
raw_schema = cast(Dict[Any, Any], raw_schema)
nodes, relationships = _convert_to_graph_document(raw_schema)
else:
nodes_set = set()
relationships = []
if not isinstance(raw_schema, str):
raw_schema = raw_schema.content
parsed_json = self.json_repair.loads(raw_schema)
for rel in parsed_json:
# Nodes need to be deduplicated using a set
nodes_set.add((rel["head"], rel["head_type"]))
nodes_set.add((rel["tail"], rel["tail_type"]))
source_node = Node(id=rel["head"], type=rel["head_type"])
target_node = Node(id=rel["tail"], type=rel["tail_type"])
relationships.append(
Relationship(
source=source_node, target=target_node, type=rel["relation"]
)
)
# Create nodes list
nodes = [Node(id=el[0], type=el[1]) for el in list(nodes_set)]
# Strict mode filtering
if self.strict_mode and (self.allowed_nodes or self.allowed_relationships):
if self.allowed_nodes:
lower_allowed_nodes = [el.lower() for el in self.allowed_nodes]
nodes = [
node for node in nodes if node.type.lower() in lower_allowed_nodes
]
relationships = [
rel
for rel in relationships
if rel.source.type.lower() in lower_allowed_nodes
and rel.target.type.lower() in lower_allowed_nodes
]
if self.allowed_relationships:
relationships = [
rel
for rel in relationships
if rel.type.lower()
in [el.lower() for el in self.allowed_relationships]
]
return GraphDocument(nodes=nodes, relationships=relationships, source=document)
def convert_to_graph_documents(
self, documents: Sequence[Document], config: Optional[RunnableConfig] = None
) -> List[GraphDocument]:
"""Convert a sequence of documents into graph documents.
Args:
documents (Sequence[Document]): The original documents.
**kwargs: Additional keyword arguments.
Returns:
Sequence[GraphDocument]: The transformed documents as graphs.
"""
return [self.process_response(document, config) for document in documents]
async def aprocess_response(
self, document: Document, config: Optional[RunnableConfig] = None
) -> GraphDocument:
"""
Asynchronously processes a single document, transforming it into a
graph document.
"""
text = document.page_content
raw_schema = await self.chain.ainvoke({"input": text}, config=config)
raw_schema = cast(Dict[Any, Any], raw_schema)
nodes, relationships = _convert_to_graph_document(raw_schema)
if self.strict_mode and (self.allowed_nodes or self.allowed_relationships):
if self.allowed_nodes:
lower_allowed_nodes = [el.lower() for el in self.allowed_nodes]
nodes = [
node for node in nodes if node.type.lower() in lower_allowed_nodes
]
relationships = [
rel
for rel in relationships
if rel.source.type.lower() in lower_allowed_nodes
and rel.target.type.lower() in lower_allowed_nodes
]
if self.allowed_relationships:
relationships = [
rel
for rel in relationships
if rel.type.lower()
in [el.lower() for el in self.allowed_relationships]
]
return GraphDocument(nodes=nodes, relationships=relationships, source=document)
async def aconvert_to_graph_documents(
self, documents: Sequence[Document], config: Optional[RunnableConfig] = None
) -> List[GraphDocument]:
"""
Asynchronously convert a sequence of documents into graph documents.
"""
tasks = [
asyncio.create_task(self.aprocess_response(document, config))
for document in documents
]
results = await asyncio.gather(*tasks)
return results

14
backend/database.py Normal file
View file

@ -0,0 +1,14 @@
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from envs import POSTGRES_DATABASE_URL
engine = create_engine(
POSTGRES_DATABASE_URL
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

10
backend/envs.py Normal file
View file

@ -0,0 +1,10 @@
#POSTGRES DB TO TRACK USERS
POSTGRES_DATABASE_URL = "postgresql+psycopg2://postgres:postgres@localhost:5432/surfsense"
# API KEY TO VERIFY
API_SECRET_KEY = "surfsense"
# Your JWT secret and algorithm
SECRET_KEY = "your_secret_key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 720

14
backend/models.py Normal file
View file

@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, String
from database import Base
from database import engine
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
# Create the database tables if they don't exist
User.metadata.create_all(bind=engine)

84
backend/prompts.py Normal file
View file

@ -0,0 +1,84 @@
from langchain_core.prompts.prompt import PromptTemplate
from langchain_core.prompts import ChatPromptTemplate
from datetime import datetime, timezone
DATE_TODAY = "Today's date is " + datetime.now(timezone.utc).astimezone().isoformat() + '\n'
CYPHER_QA_TEMPLATE = DATE_TODAY + """You are an assistant that helps to form nice and human understandable answers.
The information part contains the provided information that you must use to construct an answer.
The provided information is authoritative, you must never doubt it or try to use your internal knowledge to correct it.
Make the answer sound as a response to the question. Do not mention that you based the result on the given information.
Here are the examples:
Question: Website on which the most time was spend on?
Context:[{'d.VisitedWebPageURL': 'https://stackoverflow.com/questions/59873698/the-default-export-is-not-a-react-component-in-page-nextjs', 'totalDuration': 8889167}]
Helpful Answer: You visited https://stackoverflow.com/questions/59873698/the-default-export-is-not-a-react-component-in-page-nextjs for 8889167 milliseconds or 8889.167 seconds.
Follow this example when generating answers.
If the provided information is empty, then and only then, return exactly 'don't know' as answer.
Information:
{context}
Question: {question}
Helpful Answer:"""
CYPHER_QA_PROMPT = PromptTemplate(
input_variables=["context", "question"], template=CYPHER_QA_TEMPLATE
)
SIMILARITY_SEARCH_RAG = DATE_TODAY + """You are an assistant for question-answering tasks.
Use the following pieces of retrieved context to answer the question.
If you don't know the answer, return exactly 'don't know' as answer.
Question: {question}
Context: {context}
Answer:"""
SIMILARITY_SEARCH_PROMPT = PromptTemplate(
input_variables=["context", "question"], template=SIMILARITY_SEARCH_RAG
)
# doc_extract_chain = DOCUMENT_METADATA_EXTRACTION_PROMT | structured_llm
CYPHER_GENERATION_TEMPLATE = DATE_TODAY + """Task:Generate Cypher statement to query a graph database.
Instructions:
Use only the provided relationship types and properties in the schema.
Do not use any other relationship types or properties that are not provided.
Schema:
{schema}
Note: Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything else than for you to construct a Cypher statement.
Do not include any text except the generated Cypher statement.
The question is:
{question}"""
CYPHER_GENERATION_PROMPT = PromptTemplate(
input_variables=["schema", "question"], template=CYPHER_GENERATION_TEMPLATE
)
DOC_DESCRIPTION_TEMPLATE = """Task:Give Detailed Description of the page content of the given document.
Instructions:
Provide as much details about metadata & page content as if you need to give human readable report of this Browsing session event.
Document:
{document}
"""
DOC_DESCRIPTION_PROMPT = PromptTemplate(
input_variables=["document"], template=DOC_DESCRIPTION_TEMPLATE
)
DOCUMENT_METADATA_EXTRACTION_SYSTEM_MESSAGE = DATE_TODAY + """You are a helpful assistant. You are given a Cypher statement result after quering the Neo4j graph database.
Generate a very good Query that can be used to perform similarity search on the vector store of the Neo4j graph database"""
DOCUMENT_METADATA_EXTRACTION_PROMT = ChatPromptTemplate.from_messages([("system", DOCUMENT_METADATA_EXTRACTION_SYSTEM_MESSAGE), ("human", "{input}")])

41
backend/pydmodels.py Normal file
View file

@ -0,0 +1,41 @@
from pydantic import BaseModel, Field
from typing import List, Optional
class UserQuery(BaseModel):
query: str
neourl: str
neouser: str
neopass: str
openaikey: str
apisecretkey: str
class DescriptionResponse(BaseModel):
response: str
class DocMeta(BaseModel):
BrowsingSessionId: Optional[str] = Field(default=None, description="BrowsingSessionId of Document")
VisitedWebPageURL: Optional[str] = Field(default=None, description="VisitedWebPageURL of Document")
VisitedWebPageTitle: Optional[str] = Field(default=None, description="VisitedWebPageTitle of Document")
VisitedWebPageDateWithTimeInISOString: Optional[str] = Field(default=None, description="VisitedWebPageDateWithTimeInISOString of Document")
VisitedWebPageReffererURL: Optional[str] = Field(default=None, description="VisitedWebPageReffererURL of Document")
VisitedWebPageVisitDurationInMilliseconds: Optional[int] = Field(default=None, description="VisitedWebPageVisitDurationInMilliseconds of Document"),
VisitedWebPageContent: Optional[str] = Field(default=None, description="Visited WebPage Content in markdown of Document")
class RetrivedDocListItem(BaseModel):
metadata: DocMeta
pageContent: str
class RetrivedDocList(BaseModel):
documents: List[RetrivedDocListItem]
neourl: str
neouser: str
neopass: str
openaikey: str
apisecretkey: str
class UserQueryResponse(BaseModel):
response: str
relateddocs: List[DocMeta]

8
backend/requirements.txt Normal file
View file

@ -0,0 +1,8 @@
bcrypt
cryptography
fastapi
python-jose
python-multipart
SQLAlchemy
uvicorn
passlib

322
backend/server.py Normal file
View file

@ -0,0 +1,322 @@
from __future__ import annotations
from langchain.chains import GraphCypherQAChain
from langchain_community.graphs import Neo4jGraph
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Neo4jVector
from envs import ACCESS_TOKEN_EXPIRE_MINUTES, ALGORITHM, API_SECRET_KEY, SECRET_KEY
from prompts import CYPHER_QA_PROMPT, DOC_DESCRIPTION_PROMPT, SIMILARITY_SEARCH_PROMPT , CYPHER_GENERATION_PROMPT, DOCUMENT_METADATA_EXTRACTION_PROMT
from pydmodels import DescriptionResponse, UserQuery, DocMeta, RetrivedDocList, UserQueryResponse
from langchain_experimental.text_splitter import SemanticChunker
#Our Imps
from LLMGraphTransformer import LLMGraphTransformer
from langchain_openai import ChatOpenAI
# Auth Libs
from fastapi import FastAPI, Depends, HTTPException, Request, status
from sqlalchemy.orm import Session
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from datetime import datetime, timedelta
from passlib.context import CryptContext
from models import User
from database import SessionLocal, engine
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
@app.post("/")
def get_user_query_response(data: UserQuery, response_model=UserQueryResponse):
if(data.apisecretkey != API_SECRET_KEY):
raise HTTPException(status_code=401, detail="Unauthorized")
query = data.query
graph = Neo4jGraph(url=data.neourl, username=data.neouser, password=data.neopass)
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
max_tokens=None,
timeout=None,
api_key=data.openaikey
)
embeddings = OpenAIEmbeddings(
model="text-embedding-ada-002",
api_key=data.openaikey,
)
chain = GraphCypherQAChain.from_llm(
graph=graph,
cypher_prompt=CYPHER_GENERATION_PROMPT,
cypher_llm=llm,
verbose=True,
validate_cypher=True,
qa_prompt=CYPHER_QA_PROMPT ,
qa_llm=llm,
return_intermediate_steps=True,
top_k=5,
)
vector_index = Neo4jVector.from_existing_graph(
embeddings,
graph=graph,
search_type="hybrid",
node_label="Document",
text_node_properties=["text"],
embedding_node_property="embedding",
)
docs = vector_index.similarity_search(query,k=5)
docstoreturn = []
for doc in docs:
docstoreturn.append(
DocMeta(
BrowsingSessionId=doc.metadata["BrowsingSessionId"] if "BrowsingSessionId" in doc.metadata.keys() else "NOT AVAILABLE",
VisitedWebPageURL=doc.metadata["VisitedWebPageURL"] if "VisitedWebPageURL" in doc.metadata.keys()else "NOT AVAILABLE",
VisitedWebPageTitle=doc.metadata["VisitedWebPageTitle"] if "VisitedWebPageTitle" in doc.metadata.keys() else "NOT AVAILABLE",
VisitedWebPageDateWithTimeInISOString= doc.metadata["VisitedWebPageDateWithTimeInISOString"] if "VisitedWebPageDateWithTimeInISOString" in doc.metadata.keys() else "NOT AVAILABLE",
VisitedWebPageReffererURL= doc.metadata["VisitedWebPageReffererURL"] if "VisitedWebPageReffererURL" in doc.metadata.keys() else "NOT AVAILABLE",
VisitedWebPageVisitDurationInMilliseconds= doc.metadata["VisitedWebPageVisitDurationInMilliseconds"] if "VisitedWebPageVisitDurationInMilliseconds" in doc.metadata.keys() else None,
VisitedWebPageContent= doc.page_content if doc.page_content else "NOT AVAILABLE"
)
)
docstoreturn = [i for n, i in enumerate(docstoreturn) if i not in docstoreturn[n + 1:]]
try:
response = chain.invoke({"query": query})
if "don't know" in response["result"]:
raise Exception("No response from graph")
structured_llm = llm.with_structured_output(RetrivedDocList)
doc_extract_chain = DOCUMENT_METADATA_EXTRACTION_PROMT | structured_llm
query = doc_extract_chain.invoke(response["intermediate_steps"][1]["context"])
docs = vector_index.similarity_search(query.searchquery,k=5)
docstoreturn = []
for doc in docs:
docstoreturn.append(
DocMeta(
BrowsingSessionId=doc.metadata["BrowsingSessionId"] if "BrowsingSessionId" in doc.metadata.keys() else "NOT AVAILABLE",
VisitedWebPageURL=doc.metadata["VisitedWebPageURL"] if "VisitedWebPageURL" in doc.metadata.keys()else "NOT AVAILABLE",
VisitedWebPageTitle=doc.metadata["VisitedWebPageTitle"] if "VisitedWebPageTitle" in doc.metadata.keys() else "NOT AVAILABLE",
VisitedWebPageDateWithTimeInISOString= doc.metadata["VisitedWebPageDateWithTimeInISOString"] if "VisitedWebPageDateWithTimeInISOString" in doc.metadata.keys() else "NOT AVAILABLE",
VisitedWebPageReffererURL= doc.metadata["VisitedWebPageReffererURL"] if "VisitedWebPageReffererURL" in doc.metadata.keys() else "NOT AVAILABLE",
VisitedWebPageVisitDurationInMilliseconds= doc.metadata["VisitedWebPageVisitDurationInMilliseconds"] if "VisitedWebPageVisitDurationInMilliseconds" in doc.metadata.keys() else None,
VisitedWebPageContent= doc.page_content if doc.page_content else "NOT AVAILABLE"
)
)
docstoreturn = [i for n, i in enumerate(docstoreturn) if i not in docstoreturn[n + 1:]]
return UserQueryResponse(relateddocs=docstoreturn,response=response["result"])
except:
# Fallback to Similarity Search RAG
searchchain = SIMILARITY_SEARCH_PROMPT | llm
response = searchchain.invoke({"question": query, "context": docs})
return UserQueryResponse(relateddocs=docstoreturn,response=response.content)
# DOC DESCRIPTION
@app.post("/kb/doc")
def get_doc_description(data: UserQuery, response_model=DescriptionResponse):
if(data.apisecretkey != API_SECRET_KEY):
raise HTTPException(status_code=401, detail="Unauthorized")
document = data.query
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
max_tokens=None,
timeout=None,
api_key=data.openaikey
)
descriptionchain = DOC_DESCRIPTION_PROMPT | llm
response = descriptionchain.invoke({"document": document})
return DescriptionResponse(response=response.content)
# SAVE DOCS TO GRAPH DB
@app.post("/kb/")
def populate_graph(apires: RetrivedDocList):
if(apires.apisecretkey != API_SECRET_KEY):
raise HTTPException(status_code=401, detail="Unauthorized")
print("STARTED")
# print(apires)
graph = Neo4jGraph(url=apires.neourl, username=apires.neouser, password=apires.neopass)
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
max_tokens=None,
timeout=None,
api_key=apires.openaikey
)
embeddings = OpenAIEmbeddings(
model="text-embedding-ada-002",
api_key=apires.openaikey,
)
llm_transformer = LLMGraphTransformer(llm=llm)
raw_documents = []
for doc in apires.documents:
raw_documents.append(Document(page_content=doc.pageContent, metadata=doc.metadata))
text_splitter = SemanticChunker(embeddings=embeddings)
documents = text_splitter.split_documents(raw_documents)
graph_documents = llm_transformer.convert_to_graph_documents(documents)
graph.add_graph_documents(
graph_documents,
baseEntityLabel=True,
include_source=True
)
print("FINISHED")
return {
"success": "Graph Will be populated Shortly"
}
#AUTH CODE
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Recommended for Local Setups
# origins = [
# "http://localhost:3000", # Adjust the port if your frontend runs on a different one
# "https://yourfrontenddomain.com",
# ]
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allows all origins from the list
allow_credentials=True,
allow_methods=["*"], # Allows all methods
allow_headers=["*"], # Allows all headers
)
# Dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class UserCreate(BaseModel):
username: str
password: str
apisecretkey: str
def get_user_by_username(db: Session, username: str):
return db.query(User).filter(User.username == username).first()
def create_user(db: Session, user: UserCreate):
hashed_password = pwd_context.hash(user.password)
db_user = User(username=user.username, hashed_password=hashed_password)
db.add(db_user)
db.commit()
return "complete"
@app.post("/register")
def register_user(user: UserCreate, db: Session = Depends(get_db)):
if(user.apisecretkey != API_SECRET_KEY):
raise HTTPException(status_code=401, detail="Unauthorized")
db_user = get_user_by_username(db, username=user.username)
if db_user:
raise HTTPException(status_code=400, detail="Username already registered")
del user.apisecretkey
return create_user(db=db, user=user)
# Authenticate the user
def authenticate_user(username: str, password: str, db: Session):
user = db.query(User).filter(User.username == username).first()
if not user:
return False
if not pwd_context.verify(password, user.hashed_password):
return False
return user
# Create access token
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
@app.post("/token")
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = authenticate_user(form_data.username, form_data.password, db)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
def verify_token(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise HTTPException(status_code=403, detail="Token is invalid or expired")
return payload
except JWTError:
raise HTTPException(status_code=403, detail="Token is invalid or expired")
@app.get("/verify-token/{token}")
async def verify_user_token(token: str):
verify_token(token=token)
return {"message": "Token is valid"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)

4
extension/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
npm-debug.log
node_modules/
dist/
tmp/

12
extension/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,12 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"files.eol": "\n",
"json.schemas": [
{
"fileMatch": [
"/manifest.json"
],
"url": "http://json.schemastore.org/chrome-manifest"
}
]
}

33
extension/.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,33 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"command": "npm",
"tasks": [
{
"label": "install",
"type": "shell",
"command": "npm",
"args": ["install"]
},
{
"label": "update",
"type": "shell",
"command": "npm",
"args": ["update"]
},
{
"label": "test",
"type": "shell",
"command": "npm",
"args": ["run", "test"]
},
{
"label": "build",
"type": "shell",
"group": "build",
"command": "npm",
"args": ["run", "watch"]
}
]
}

21
extension/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Tomofumi Chiba
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

70
extension/README.md Normal file
View file

@ -0,0 +1,70 @@
# Chrome Extension TypeScript Starter
![build](https://github.com/chibat/chrome-extension-typescript-starter/workflows/build/badge.svg)
Chrome Extension, TypeScript and Visual Studio Code
## Prerequisites
* [node + npm](https://nodejs.org/) (Current Version)
## Option
* [Visual Studio Code](https://code.visualstudio.com/)
## Includes the following
* TypeScript
* Webpack
* React
* Jest
* Example Code
* Chrome Storage
* Options Version 2
* content script
* count up badge number
* background
## Project Structure
* src/typescript: TypeScript source files
* src/assets: static files
* dist: Chrome Extension directory
* dist/js: Generated JavaScript files
## Setup
```
npm install
```
## Import as Visual Studio Code project
...
## Build
```
npm run build
```
## Build in watch mode
### terminal
```
npm run watch
```
### Visual Studio Code
Run watch mode.
type `Ctrl + Shift + B`
## Load extension to chrome
Load `dist` directory
## Test
`npx jest` or `npm run test`

8
extension/jest.config.js Normal file
View file

@ -0,0 +1,8 @@
module.exports = {
"roots": [
"src"
],
"transform": {
"^.+\\.ts$": ["ts-jest", { tsconfig: "tsconfig.test.json" }]
},
};

5033
extension/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

52
extension/package.json Normal file
View file

@ -0,0 +1,52 @@
{
"name": "chrome-extension-typescript-starter",
"version": "1.0.0",
"description": "chrome-extension-typescript-starter",
"main": "index.js",
"scripts": {
"watch": "webpack --config webpack/webpack.dev.js --watch",
"build": "webpack --config webpack/webpack.prod.js",
"clean": "rimraf dist",
"test": "npx jest",
"style": "prettier --write \"src/**/*.{ts,tsx}\""
},
"author": "",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/chibat/chrome-extension-typescript-starter.git"
},
"dependencies": {
"dom-to-semantic-markdown": "^1.2.0",
"postcss-loader": "^8.1.1",
"react": "^18.2.0",
"react-chrome-extension-router": "^1.4.0",
"react-dom": "^18.2.0",
"react-hooks-global-state": "^2.1.0",
"react-router-dom": "^6.26.0",
"react-toastify": "^10.0.5"
},
"devDependencies": {
"@types/chrome": "0.0.158",
"@types/jest": "^29.5.0",
"@types/json2md": "^1.5.4",
"@types/react": "^18.0.29",
"@types/react-dom": "^18.0.11",
"autoprefixer": "^10.4.19",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "^7.1.2",
"glob": "^7.1.6",
"jest": "^29.5.0",
"postcss": "^8.4.40",
"prettier": "^2.2.1",
"rimraf": "^3.0.2 ",
"style-loader": "^4.0.0",
"tailwindcss": "^3.4.7",
"ts-jest": "^29.1.0",
"ts-loader": "^8.0.0",
"typescript": "^5.0.4",
"webpack": "^5.76.0",
"webpack-cli": "^4.0.0",
"webpack-merge": "^5.0.0"
}
}

4622
extension/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

BIN
extension/public/brain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,38 @@
{
"manifest_version": 3,
"name": "SurfSense",
"description": "Extension to collect Browsing History for SurfSense.",
"version": "0.0.1",
"action": {
"default_popup": "popup.html"
},
"icons": {
"16": "icon-16.png",
"32": "icon-32.png",
"48": "icon-48.png",
"128": "icon-128.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["js/content_script.js"]
}
],
"background": {
"service_worker": "js/background.js"
},
"permissions": [
"storage",
"scripting",
"unlimitedStorage"
],
"host_permissions": [
"<all_urls>"
]
}

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>My Test Extension Options</title>
<script src="js/vendor.js"></script>
</head>
<body>
<div id="root"></div>
<script src="js/options.js"></script>
</body>
</html>

View file

@ -0,0 +1,13 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Getting Started Extension's Popup</title>
<script src="js/vendor.js"></script>
</head>
<body>
<div id="root"></div>
<script src="js/popup.js"></script>
</body>
</html>

View file

@ -0,0 +1,9 @@
import { sum } from "../sum";
test("1 + 1 = 2", () => {
expect(sum(1, 1)).toBe(2);
});
test("1 + 2 != 2", () => {
expect(sum(1, 2)).not.toBe(2);
});

View file

@ -0,0 +1,39 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.loading{
margin: 0;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
height: 80vh;
}
body {
min-width: 400px;
}
.btn1 {
width: 50px;
height: 50px;
border-radius: 25px;
border: none;
background-color: hotpink;
color: white;
font-size: 1.2em;
}
@keyframes move {
25% {
transform: translateY(50px);
}
50% {
background-color: dodgerblue;
transform: translateY(0px);
}
75% {
transform: translateY(-50px);
}
}

454
extension/src/background.ts Normal file
View file

@ -0,0 +1,454 @@
import {
initWebHistory,
getRenderedHtml,
initQueues,
} from "./commons";
import { WebHistory } from "./interfaces";
chrome.tabs.onCreated.addListener(async (tab: any) => {
try {
await initWebHistory(tab.id);
await initQueues(tab.id);
} catch (error) {
console.log(error);
}
});
chrome.tabs.onUpdated.addListener(
async (tabId: number, changeInfo: any, tab: any) => {
if (changeInfo.status === "complete" && tab.url) {
await initWebHistory(tab.id);
await initQueues(tab.id);
const result = await chrome.scripting.executeScript({
// @ts-ignore
target: { tabId: tab.id },
// @ts-ignore
function: getRenderedHtml,
});
let toPushInTabHistory = result[0].result; // const { renderedHtml, title, url, entryTime } = result[0].result;
let urlQueueListObj = await chrome.storage.local.get(["urlQueueList"]);
let timeQueueListObj = await chrome.storage.local.get(["timeQueueList"]);
urlQueueListObj.urlQueueList
.find((data: WebHistory) => data.tabsessionId === tabId)
.urlQueue.push(toPushInTabHistory.url);
timeQueueListObj.timeQueueList
.find((data: WebHistory) => data.tabsessionId === tabId)
.timeQueue.push(toPushInTabHistory.entryTime);
await chrome.storage.local.set({
urlQueueList: urlQueueListObj.urlQueueList,
});
await chrome.storage.local.set({
timeQueueList: timeQueueListObj.timeQueueList,
});
}
}
);
chrome.tabs.onRemoved.addListener(async (tabId: number, removeInfo: object) => {
let urlQueueListObj = await chrome.storage.local.get(["urlQueueList"]);
let timeQueueListObj = await chrome.storage.local.get(["timeQueueList"]);
if(urlQueueListObj.urlQueueList && timeQueueListObj.timeQueueList){
const urlQueueListToSave = urlQueueListObj.urlQueueList.map((element: WebHistory) => {
if(element.tabsessionId !== tabId){
return element
}
})
const timeQueueListSave = timeQueueListObj.timeQueueList.map((element: WebHistory) => {
if(element.tabsessionId !== tabId){
return element
}
})
await chrome.storage.local.set({
urlQueueList: urlQueueListToSave.filter((item: any) => item),
});
await chrome.storage.local.set({
timeQueueList: timeQueueListSave.filter((item: any) => item),
});
}
});
///// IGONRE THESE COMMENTS THESE CONTAINS SOME IDEAS THAT NEVER WORKED AS INTENTDED
// await initWebHistory(tabId);
// console.log("tab", tab);
// if (tab.status === "loading") {
// if (tab.url) {
// const autotrackerFlag = await chrome.storage.local.get(["autoTracker"]);
// const lastUrlObj = await chrome.storage.local.get(["lastUrl"]);
// if (autotrackerFlag.autoTracker) {
// if (lastUrlObj.lastUrl[tabId] !== "START") {
// console.log("loading");
// console.log(lastUrlObj.lastUrl[tabId]);
// //update last entry duration
// try {
// const lastEntryTimeObj = await chrome.storage.local.get([
// "lastEntryTime",
// ]);
// let webhistoryObj = await chrome.storage.local.get([
// "webhistory",
// ]);
// const webHistoryLength = webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tabId
// ).tabHistory.length;
// if(webHistoryLength > 0){
// webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tabId
// ).tabHistory[webHistoryLength - 1].duration =
// Date.now() - lastEntryTimeObj.lastEntryTime[tabId];
// }
// await chrome.storage.local.set({
// webhistory: webhistoryObj.webhistory,
// });
// } catch (error) {
// console.log(error);
// }
// }
// }
// }
// }
// const autotrackerFlag = await chrome.storage.local.get(["autoTracker"]);
// if (!autotrackerFlag.autoTracker) {
// await initURlQueue(tab.id);
// }
// const lastUrl = {
// // @ts-ignore
// [tab.id]: "START",
// };
// // console.log(lastUrl);
// await chrome.storage.local.set({
// lastUrl: lastUrl,
// });
// const lastEntryTime = {
// // @ts-ignore
// [tab.id]: Date.now(),
// };
// // console.log(lastUrl);
// await chrome.storage.local.set({
// lastEntryTime: lastEntryTime,
// });
// let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
// const webHistoryOfTabId = webhistoryObj.webhistory.filter(
// (data: WebHistory) => {
// return data.tabsessionId === tab.id;
// }
// );
// let tabhistory = webHistoryOfTabId[0].tabHistory;
// if (tabhistory.length === 0) {
// toPushInTabHistory.reffererUrl = "START";
// } else {
// toPushInTabHistory.reffererUrl = tabhistory[tabhistory.length - 1].url;
// tabhistory[tabhistory.length - 1].duration =
// toPushInTabHistory.entryTime -
// tabhistory[tabhistory.length - 1].entryTime;
// }
// tabhistory.push(toPushInTabHistory);
// //Update Webhistory
// try {
// webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tab.id
// ).tabHistory = tabhistory;
// await chrome.storage.local.set({
// webhistory: webhistoryObj.webhistory,
// });
// } catch (error) {
// console.log(error);
// }
// const autotrackerFlag = await chrome.storage.local.get(["autoTracker"]);
// if (autotrackerFlag.autoTracker) {
// const result = await chrome.scripting.executeScript({
// // @ts-ignore
// target: { tabId: tab.id },
// // @ts-ignore
// function: getRenderedHtml,
// });
// let toPushInTabHistory = result[0].result; // const { renderedHtml, title, url, entryTime } = result[0].result;
// // //Updates 'tabhistory'
// let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
// const webHistoryOfTabId = webhistoryObj.webhistory.filter(
// (data: WebHistory) => {
// return data.tabsessionId === tab.id;
// }
// );
// let tabhistory = webHistoryOfTabId[0].tabHistory;
// // let lastUrlObj = await chrome.storage.local.get(["lastUrl"]);
// const lastEntryTimeObj = await chrome.storage.local.get([
// "lastEntryTime",
// ]);
// lastEntryTimeObj.lastEntryTime[tabId] = Date.now();
// await chrome.storage.local.set({
// lastEntryTime: lastEntryTimeObj.lastEntryTime,
// });
// //When first entry
// if (tabhistory.length === 0) {
// let lastUrlObj = await chrome.storage.local.get(["lastUrl"]);
// lastUrlObj.lastUrl[tabId] = tab.url;
// await chrome.storage.local.set({
// lastUrl: lastUrlObj.lastUrl,
// });
// toPushInTabHistory.reffererUrl = "START";
// toPushInTabHistory.entryTime = Date.now();
// tabhistory.push(toPushInTabHistory);
// try {
// webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tab.id
// ).tabHistory = tabhistory;
// await chrome.storage.local.set({
// webhistory: webhistoryObj.webhistory,
// });
// } catch (error) {
// console.log(error);
// }
// } else {
// const lastUrlObj = await chrome.storage.local.get(["lastUrl"]);
// toPushInTabHistory.reffererUrl = lastUrlObj.lastUrl[tabId];
// toPushInTabHistory.entryTime = Date.now();
// tabhistory.push(toPushInTabHistory);
// try {
// webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tab.id
// ).tabHistory = tabhistory;
// // console.log("webhistory",webhistoryObj);
// await chrome.storage.local.set({
// webhistory: webhistoryObj.webhistory,
// });
// } catch (error) {
// console.log(error);
// }
// lastUrlObj.lastUrl[tabId] = tab.url;
// await chrome.storage.local.set({
// lastUrl: lastUrlObj.lastUrl,
// });
// }
// } else {
// await initURlQueue(tab.id);
// let urlQueue = await chrome.storage.local.get(["urlQueue"]);
// // console.log("urlQueue", urlQueue);
// urlQueue.urlQueue[tabId].push(tab.url)
// urlQueue.urlQueue[tabId] = [...new Set(urlQueue.urlQueue[tabId])]
// await chrome.storage.local.set({
// urlQueue: urlQueue.urlQueue,
// });
// const lastEntryTimeObj = await chrome.storage.local.get([
// "lastEntryTime",
// ]);
// lastEntryTimeObj.lastEntryTime[tabId] = Date.now();
// await chrome.storage.local.set({
// lastEntryTime: lastEntryTimeObj.lastEntryTime,
// });
// }
// chrome.tabs.onRemoved.addListener(async (tabId: number, removeInfo: object) => {
// const autotrackerFlag = await chrome.storage.local.get(["autoTracker"]);
// //duration, referURL edge conditions
// let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
// const webHistoryLength = webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tabId
// ).tabHistory.length;
// if (webHistoryLength > 0) {
// if (
// !webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tabId
// ).tabHistory[webHistoryLength - 1].duration
// ) {
// webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tabId
// ).tabHistory[webHistoryLength - 1].duration =
// Date.now() -
// webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tabId
// ).tabHistory[webHistoryLength - 1].entryTime;
// }
// }
// let urlQueueLocal = await chrome.storage.local.get(["urlQueue"]);
// let timeQueueLocal = await chrome.storage.local.get(["timeQueue"]);
// delete urlQueueLocal.urlQueue[tabId]
// delete timeQueueLocal.timeQueue[tabId]
// await chrome.storage.local.set({
// urlQueue: urlQueueLocal.urlQueue,
// });
// await chrome.storage.local.set({
// timeQueue: timeQueueLocal.timeQueue,
// });
// // if (autotrackerFlag.autoTracker) {
// // try {
// // const lastEntryTimeObj = await chrome.storage.local.get([
// // "lastEntryTime",
// // ]);
// // let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
// // const webHistoryLength = webhistoryObj.webhistory.find(
// // (data: WebHistory) => data.tabsessionId === tabId
// // ).tabHistory.length;
// // if (webHistoryLength > 0) {
// // webhistoryObj.webhistory.find(
// // (data: WebHistory) => data.tabsessionId === tabId
// // ).tabHistory[webHistoryLength - 1].duration =
// // Date.now() - lastEntryTimeObj.lastEntryTime[tabId];
// // }
// // await chrome.storage.local.set({
// // webhistory: webhistoryObj.webhistory,
// // });
// // } catch (error) {
// // console.log(error);
// // }
// // } else {
// // await initURlQueue(tabId);
// // let urlQueue = await chrome.storage.local.get(["urlQueue"]);
// // delete urlQueue.urlQueue[tabId];
// // chrome.storage.local.set({
// // urlQueue: urlQueue.urlQueue,
// // });
// // }
// });
// if (tabhistory.length > 0) {
// //updates duration of last entry in 'tabhistory'
// tabhistory[tabhistory.length - 1].duration =
// toPushInTabHistory.entryTime -
// tabhistory[tabhistory.length - 1].entryTime;
// //Update refferer Url
// toPushInTabHistory.reffererUrl = tabhistory[tabhistory.length - 1].url;
// if (tabhistory.length == 1) {
// tabhistory[0].reffererUrl = "START";
// }
// }
// tabhistory.push(toPushInTabHistory);
// //Set Updated tabhistory
// try {
// webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tab.id
// ).tabHistory = tabhistory;
// // console.log("webhistory",webhistoryObj);
// await chrome.storage.local.set({
// webhistory: webhistoryObj.webhistory,
// });
// } catch (error) {
// console.log(error);
// }
// try {
// let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
// const webHistoryLength = webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tabId
// ).tabHistory.length;
// const lastWebPageEntryTime = webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tabId
// ).tabHistory[webHistoryLength - 1].entryTime;
// webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tabId
// ).tabHistory[webHistoryLength - 1].duration =
// Date.now() - lastWebPageEntryTime;
// //Edge Condition of reffererUrl
// if (webHistoryLength == 1) {
// webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tabId
// ).tabHistory[0].reffererUrl = "START";
// }
// //Sets 'webhistory'
// try {
// await chrome.storage.local.set({ webhistory: webhistoryObj.webhistory });
// // const result = await chrome.storage.local.get(["webhistory"]);
// // console.log("webhistoryinRemoved",result);
// } catch (error) {
// console.log(error);
// }
// } catch (error) {
// console.log(error);
// }
// await chrome.storage.local.set({ id: tab.id });
// await chrome.storage.local.set({ tabhistory: [] });
// const result = await chrome.storage.local.get(["id"]);
// console.log(result);
// const tabid = await chrome.storage.local.get(["id"]);
// const toPushinWebHostory = {
// tabsessionId: tabid.id,
// tabHistory: tabhistory,
// };
// //Updates 'webhistory'
// const webhistoryObj = await chrome.storage.local.get(["webhistory"]);
// // console.log("WEBH", webhistoryObj);
// const webhistory = webhistoryObj.webhistory;
// webhistory.push(toPushinWebHostory);
// //Sets 'webhistory'
// try {
// await chrome.storage.local.set({ webhistory: webhistory });
// const result = await chrome.storage.local.get(["webhistory"]);
// console.log("RES",result)
// } catch (error) {
// console.log(error);
// }
// try {
// let lastUrlObj = await chrome.storage.local.get(["lastUrl"]);
// // console.log("Before Update", lastUrlObj);
// if(tab.url !== lastUrlObj.lastUrl[tabId]){
// // console.log("Before Update", lastUrlObj);
// lastUrlObj.lastUrl[tabId] = tab.url;
// // console.log("After Update", lastUrlObj);
// await chrome.storage.local.set({
// lastUrl: lastUrlObj.lastUrl,
// });
// //Update DURATION of old url
// }
// // // const lastUrl = await chrome.storage.local.get(["lastUrl"]);
// // await chrome.storage.local.set({
// // lastUrl: lastUrlObj.lastUrl,
// // });
// } catch (error) {
// console.log(error);
// }

126
extension/src/commons.tsx Normal file
View file

@ -0,0 +1,126 @@
import { WebHistory } from "./interfaces";
export const emptyArr: any[] = [];
export const initQueues = async (tabId: number) => {
let urlQueueListObj = await chrome.storage.local.get(["urlQueueList"]);
let timeQueueListObj = await chrome.storage.local.get(["timeQueueList"]);
if(!urlQueueListObj.urlQueueList && !timeQueueListObj.timeQueueList){
await chrome.storage.local.set({
urlQueueList: [{tabsessionId: tabId, urlQueue: []}],
});
await chrome.storage.local.set({
timeQueueList: [{tabsessionId: tabId, timeQueue: []}],
});
return
}
if(urlQueueListObj.urlQueueList && timeQueueListObj.timeQueueList){
const isUrlQueueThere = urlQueueListObj.urlQueueList.find((data: WebHistory) => data.tabsessionId === tabId)
const isTimeQueueThere = timeQueueListObj.timeQueueList.find((data: WebHistory) => data.tabsessionId === tabId)
if(!isUrlQueueThere){
urlQueueListObj.urlQueueList.push({tabsessionId: tabId, urlQueue: []})
await chrome.storage.local.set({
urlQueueList: urlQueueListObj.urlQueueList,
});
}
if(!isTimeQueueThere){
timeQueueListObj.timeQueueList.push({tabsessionId: tabId, timeQueue: []})
await chrome.storage.local.set({
timeQueueList: timeQueueListObj.timeQueueList,
});
}
return
}
};
export function getRenderedHtml() {
return {
url: window.location.href,
entryTime: Date.now(),
title: document.title,
renderedHtml: document.documentElement.outerHTML,
};
}
export const initWebHistory = async (tabId: number) => {
const result = await chrome.storage.local.get(["webhistory"]);
if (!result.webhistory) {
await chrome.storage.local.set({ webhistory: emptyArr });
return
}
const ifIdExists = result.webhistory.find(
(data: WebHistory) => data.tabsessionId === tabId
);
if (ifIdExists === undefined) {
let webHistory = result.webhistory;
const initData = {
tabsessionId: tabId,
tabHistory: emptyArr,
};
webHistory.push(initData)
try {
await chrome.storage.local.set({ webhistory: webHistory });
return ;
} catch (error) {
console.log(error)
}
} else {
return;
}
};
export function toIsoString(date: Date) {
var tzo = -date.getTimezoneOffset(),
dif = tzo >= 0 ? '+' : '-',
pad = function (num: number) {
return (num < 10 ? '0' : '') + num;
};
return date.getFullYear() +
'-' + pad(date.getMonth() + 1) +
'-' + pad(date.getDate()) +
'T' + pad(date.getHours()) +
':' + pad(date.getMinutes()) +
':' + pad(date.getSeconds()) +
dif + pad(Math.floor(Math.abs(tzo) / 60)) +
':' + pad(Math.abs(tzo) % 60);
}
export const webhistoryToLangChainDocument = (tabId: number, tabHistory: any[]) => {
let toSaveFinally = []
for (let j = 0; j < tabHistory.length; j++) {
const mtadata = {
"BrowsingSessionId": `${tabId}`,
"VisitedWebPageURL": `${tabHistory[j].url}`,
"VisitedWebPageTitle": `${tabHistory[j].title}`,
"VisitedWebPageDateWithTimeInISOString": `${toIsoString(new Date(tabHistory[j].entryTime))}`,
"VisitedWebPageReffererURL": `${tabHistory[j].reffererUrl}`,
"VisitedWebPageVisitDurationInMilliseconds": tabHistory[j].duration,
}
toSaveFinally.push({
metadata: mtadata,
pageContent: tabHistory[j].pageContentMarkdown
})
}
return toSaveFinally
}

View file

@ -0,0 +1,3 @@
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
});

2
extension/src/env.tsx Normal file
View file

@ -0,0 +1,2 @@
export const API_SECRET_KEY = "surfsense"
export const BACKEND_URL = "http://127.0.0.1:8000"

View file

@ -0,0 +1,4 @@
export interface WebHistory {
tabsessionId: number;
tabHistory: any[];
}

77
extension/src/options.tsx Normal file
View file

@ -0,0 +1,77 @@
// import React, { useEffect, useState } from "react";
// import { createRoot } from "react-dom/client";
// const Options = () => {
// const [color, setColor] = useState<string>("");
// const [status, setStatus] = useState<string>("");
// const [like, setLike] = useState<boolean>(false);
// useEffect(() => {
// // Restores select box and checkbox state using the preferences
// // stored in chrome.storage.
// chrome.storage.sync.get(
// {
// favoriteColor: "red",
// likesColor: true,
// },
// (items) => {
// setColor(items.favoriteColor);
// setLike(items.likesColor);
// }
// );
// }, []);
// const saveOptions = () => {
// // Saves options to chrome.storage.sync.
// chrome.storage.sync.set(
// {
// favoriteColor: color,
// likesColor: like,
// },
// () => {
// // Update status to let user know options were saved.
// setStatus("Options saved.");
// const id = setTimeout(() => {
// setStatus("");
// }, 1000);
// return () => clearTimeout(id);
// }
// );
// };
// return (
// <>
// <div>
// Favorite color: <select
// value={color}
// onChange={(event) => setColor(event.target.value)}
// >
// <option value="red">red</option>
// <option value="green">green</option>
// <option value="blue">blue</option>
// <option value="yellow">yellow</option>
// </select>
// </div>
// <div>
// <label>
// <input
// type="checkbox"
// checked={like}
// onChange={(event) => setLike(event.target.checked)}
// />
// I like colors.
// </label>
// </div>
// <div>{status}</div>
// <button onClick={saveOptions}>Save</button>
// </>
// );
// };
// const root = createRoot(document.getElementById("root")!);
// root.render(
// <React.StrictMode>
// <Options />
// </React.StrictMode>
// );

View file

@ -0,0 +1,75 @@
import React, { useState } from "react";
import { goTo } from "react-chrome-extension-router";
import { Popup } from "../popup";
export const FillEnvVariables = () => {
const [neourl, setNeourl] = useState('');
const [neouser, setNeouser] = useState('');
const [neopass, setNeopass] = useState('');
const [openaikey, setOpenaiKey] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const validateForm = () => {
if (!neourl || !neouser || !neopass || !openaikey) {
setError('All values are required');
return false;
}
setError('');
return true;
};
const handleSubmit = async (event: { preventDefault: () => void; }) => {
event.preventDefault();
if (!validateForm()) return;
setLoading(true);
localStorage.setItem('neourl', neourl);
localStorage.setItem('neouser', neouser);
localStorage.setItem('neopass', neopass);
localStorage.setItem('openaikey', openaikey);
setLoading(false);
goTo(Popup)
};
return (
<section className="dark bg-gray-50 dark:bg-gray-900">
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
SurfSense
</a>
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Required Values
</h1>
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J URL</label>
<input type="text" value={neourl} onChange={(e) => setNeourl(e.target.value)} name="neourl" id="neourl" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Username</label>
<input type="text" value={neouser} onChange={(e) => setNeouser(e.target.value)} name="neouser" id="neouser" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Password</label>
<input type="text" value={neopass} onChange={(e) => setNeopass(e.target.value)} name="neopass" id="neopass" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">OpenAI API Key</label>
<input type="text" value={openaikey} onChange={(e) => setOpenaiKey(e.target.value)} name="openaikey" id="openaikey" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
</div>
<button type="submit" className="mt-4 w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">{loading ? 'Saving....' : 'Save & Proceed'}</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
</div>
</div>
</div>
</section>
)
}

View file

@ -0,0 +1,28 @@
import React from 'react'
const Loading = () => {
return (
<><div className="flex flex-col items-center justify-center px-6 pt-2 pb-12 mx-auto md:h-screen lg:py-0">
<div className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
SurfSense
</div>
<div className="loading">
{"S A V I N G".split(" ").map((v, i) => (
<button
className="btn1"
style={{ animation: `move linear 0.9s infinite ${i / 10}s` }}
key={v}
>
{v}
</button>
))}
</div>
</div>
</>
)
}
export default Loading

View file

@ -0,0 +1,94 @@
import React, { useState } from "react";
import { goTo } from "react-chrome-extension-router";
import { Popup } from "../popup";
export const LoginForm = () => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const validateForm = () => {
if (!username || !password) {
setError('Username and password are required');
return false;
}
setError('');
return true;
};
const handleSubmit = async (event: { preventDefault: () => void; }) => {
event.preventDefault();
if (!validateForm()) return;
setLoading(true);
const formDetails = new URLSearchParams();
formDetails.append('username', username);
formDetails.append('password', password);
try {
const response = await fetch('http://localhost:8000/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formDetails,
});
setLoading(false);
if (response.ok) {
const data = await response.json();
localStorage.setItem('token', data.access_token);
goTo(Popup);
} else {
const errorData = await response.json();
setError(errorData.detail || 'Authentication failed!');
}
} catch (error) {
setLoading(false);
setError('An error occurred. Please try again later.');
}
};
// const goToRegister = async () => {
// console.log("Reg")
// goTo(RegisterForm)
// }
return (
<>
<section className="dark bg-gray-50 dark:bg-gray-900">
{/* <div onClick={() => clearMem}>CLEAR MEM</div> */}
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img className="w-8 h-8 mr-2" src={"./icon-128.png"} alt="logo" />
SurfSense
</a>
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Sign in to your account
</h1>
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} name="password" id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
</div>
<button type="submit" disabled={loading} className="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">{loading ? 'Logging in...' : 'Login'}</button>
<p className="text-sm font-light text-gray-500 dark:text-gray-400">
Dont have an account yet? <a href="http://localhost:3000/signup" className="font-medium text-primary-600 hover:underline dark:text-primary-500" >Sign up</a>
</p>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
</div>
</div>
</div>
</section>
</>
);
}

582
extension/src/popup.tsx Normal file
View file

@ -0,0 +1,582 @@
import React, { useEffect, useState } from "react";
import {
goTo,
Router,
} from 'react-chrome-extension-router';
import { createRoot } from "react-dom/client";
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import "./assets/tailwind.css"
import { convertHtmlToMarkdown } from "dom-to-semantic-markdown";
import { WebHistory } from "./interfaces";
import { webhistoryToLangChainDocument, getRenderedHtml, emptyArr } from "./commons";
import Loading from "./pages/Loading";
import { LoginForm } from "./pages/LoginForm";
import { FillEnvVariables } from "./pages/EnvVarSettings";
import { API_SECRET_KEY, BACKEND_URL } from "./env";
export async function clearMem(): Promise<void> {
try {
let result = await chrome.storage.local.get(["webhistory"]);
if (!result.webhistory) {
return
}
//Main Cleanup COde
chrome.tabs.query({}, async (tabs) => {
//Get Active Tabs Ids
// console.log("Event Tabs",tabs)
let actives = tabs.map((tab) => {
if (tab.id) {
return tab.id
}
})
actives = actives.filter((item: any) => item)
//Only retain which is still active
const newHistory = result.webhistory.map((element: any) => {
//@ts-ignore
if (actives.includes(element.tabsessionId)) {
return element
}
})
await chrome.storage.local.set({ webhistory: newHistory.filter((item: any) => item) });
toast.info("History Store Deleted!", {
position: "bottom-center"
});
});
} catch (error) {
console.log(error);
}
}
export const Popup = () => {
const [noOfWebPages, setNoOfWebPages] = useState<number>(0);
const [loading, setLoading] = useState(true);
useEffect(() => {
const verifyToken = async () => {
const token = localStorage.getItem('token');
console.log(token)
try {
const response = await fetch(`${BACKEND_URL}/verify-token/${token}`);
if (!response.ok) {
throw new Error('Token verification failed');
}else{
const NEO4JURL = localStorage.getItem('neourl');
const NEO4JUSERNAME = localStorage.getItem('neouser');
const NEO4JPASSWORD = localStorage.getItem('neopass');
const OPENAIKEY = localStorage.getItem('openaikey');
const check = (NEO4JURL && NEO4JUSERNAME && NEO4JPASSWORD && OPENAIKEY)
if(!check){
goTo(FillEnvVariables);
}
}
} catch (error) {
localStorage.removeItem('token');
goTo(LoginForm);
}
};
verifyToken();
setLoading(false)
}, []);
useEffect(() => {
async function onLoad() {
try {
chrome.storage.onChanged.addListener(
(changes: any, areaName: string) => {
if (changes.webhistory) {
// console.log("changes.webhistory", changes.webhistory)
const webhistory = changes.webhistory.newValue;
let sum = 0
webhistory.forEach((element: any) => {
sum = sum + element.tabHistory.length
});
setNoOfWebPages(sum)
}
// console.log(changes)
// console.log(areaName)
}
);
const webhistoryObj = await chrome.storage.local.get(["webhistory"]);
if (webhistoryObj.webhistory.length) {
const webhistory = webhistoryObj.webhistory;
if (webhistoryObj) {
let sum = 0
webhistory.forEach((element: any) => {
sum = sum + element.tabHistory.length
});
setNoOfWebPages(sum)
}
} else {
setNoOfWebPages(0)
}
} catch (error) {
console.log(error);
}
}
onLoad()
}, []);
const saveData = async () => {
try {
// setLoading(true);
const webhistoryObj = await chrome.storage.local.get(["webhistory"]);
const webhistory = webhistoryObj.webhistory;
if (webhistory) {
let processedHistory: any[] = []
let newHistoryAfterCleanup: any[] = []
webhistory.forEach((element: any) => {
let tabhistory = element.tabHistory;
for (let i = 0; i < tabhistory.length; i++) {
tabhistory[i].pageContentMarkdown = convertHtmlToMarkdown(tabhistory[i].renderedHtml, {
extractMainContent: true,
enableTableColumnTracking: true,
})
delete tabhistory[i].renderedHtml
}
processedHistory.push({
tabsessionId: element.tabsessionId,
tabHistory: tabhistory,
})
newHistoryAfterCleanup.push({
tabsessionId: element.tabsessionId,
tabHistory: emptyArr,
})
});
await chrome.storage.local.set({ webhistory: newHistoryAfterCleanup });
let toSaveFinally = []
for (let i = 0; i < processedHistory.length; i++) {
const markdownFormat = webhistoryToLangChainDocument(processedHistory[i].tabsessionId, processedHistory[i].tabHistory)
toSaveFinally.push(...markdownFormat)
}
// console.log("SAVING", toSaveFinally)
const toSend = {
documents: toSaveFinally,
neourl: localStorage.getItem('neourl'),
neouser: localStorage.getItem('neouser'),
neopass: localStorage.getItem('neopass'),
openaikey: localStorage.getItem('openaikey'),
apisecretkey: API_SECRET_KEY
}
// console.log("toSend",toSend)
const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(toSend),
};
toast.info("Save Job Initiated.", {
position: "bottom-center"
});
const response = await fetch(`${BACKEND_URL}/kb/`, requestOptions);
const res = await response.json();
if (res.success) {
toast.success("Save Job Completed.", {
position: "bottom-center",
autoClose: false
});
}
}
} catch (error) {
console.log(error);
}
};
// async function showMem(): Promise<void> {
// // localStorage.removeItem('token');
// // await chrome.storage.local.clear()
// const webhistoryObj = await chrome.storage.local.get(["webhistory"]);
// const urlQueue = await chrome.storage.local.get(["urlQueueList"]);
// const timeQueue = await chrome.storage.local.get(["timeQueueList"]);
// console.log("CURR MEM", webhistoryObj, urlQueue, timeQueue);
// // await chrome.storage.local.set({
// // urlQueueList: urlQueueListObj.urlQueueList,
// // });
// // await chrome.storage.local.set({
// // timeQueueList: timeQueueListObj.timeQueueList,
// // });
// // clearMem()
// }
async function logOut(): Promise<void> {
localStorage.removeItem('token');
goTo(LoginForm)
}
async function saveCurrSnapShot(): Promise<void> {
chrome.tabs.query({ active: true, currentWindow: true }, async function (tabs) {
const tab = tabs[0];
if (tab.id) {
// await initWebHistory(tab.id);
// await initQueues(tab.id);
const tabId: number = tab.id
const result = await chrome.scripting.executeScript({
// @ts-ignore
target: { tabId: tab.id },
// @ts-ignore
function: getRenderedHtml,
});
let toPushInTabHistory = result[0].result; // const { renderedHtml, title, url, entryTime } = result[0].result;
// //Updates 'tabhistory'
let webhistoryObj = await chrome.storage.local.get(["webhistory"]);
const webHistoryOfTabId = webhistoryObj.webhistory.filter(
(data: WebHistory) => {
return data.tabsessionId === tab.id;
}
);
let tabhistory = webHistoryOfTabId[0].tabHistory;
const urlQueueListObj = await chrome.storage.local.get(["urlQueueList"]);
const timeQueueListObj = await chrome.storage.local.get(["timeQueueList"]);
const isUrlQueueThere = urlQueueListObj.urlQueueList.find((data: WebHistory) => data.tabsessionId === tabId)
const isTimeQueueThere = timeQueueListObj.timeQueueList.find((data: WebHistory) => data.tabsessionId === tabId)
// console.log(isUrlQueueThere)
// console.log(isTimeQueueThere)
// console.log(isTimeQueueThere.timeQueue[isTimeQueueThere.length - 1])
toPushInTabHistory.duration = toPushInTabHistory.entryTime - isTimeQueueThere.timeQueue[isTimeQueueThere.timeQueue.length - 1]
if (isUrlQueueThere.urlQueue.length == 1) {
toPushInTabHistory.reffererUrl = 'START'
}
if (isUrlQueueThere.urlQueue.length > 1) {
toPushInTabHistory.reffererUrl = isUrlQueueThere.urlQueue[isUrlQueueThere.urlQueue.length - 2];
}
tabhistory.push(toPushInTabHistory);
// console.log(toPushInTabHistory)
//Update Webhistory
try {
webhistoryObj.webhistory.find(
(data: WebHistory) => data.tabsessionId === tab.id
).tabHistory = tabhistory;
await chrome.storage.local.set({
webhistory: webhistoryObj.webhistory,
});
} catch (error) {
console.log(error);
}
toast.success("Saved Snapshot !", {
position: "bottom-center"
});
}
});
}
if (loading) {
return <Loading />;
} else {
return (
<section className="dark bg-gray-50 dark:bg-gray-900">
{/* <div onClick={() => showMem()}>ShowMem</div> */}
<div className="flex flex-col items-center justify-center px-4 pt-4 pb-12 mx-auto md:h-screen lg:py-0">
<div className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
SurfSense
</div>
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<div className="flex justify-between">
<button type="button" onClick={() => goTo(FillEnvVariables)} className="px-3 py-2 text-xs font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="lucide lucide-settings"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></svg>
</button>
<button type="button" onClick={() => logOut()} className="px-3 py-2 text-xs font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" className="lucide lucide-log-out"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><polyline points="16 17 21 12 16 7" /><line x1="21" x2="9" y1="12" y2="12" /></svg>
</button>
</div>
<div className="flex flex-col gap-3">
<div className="block max-w-sm p-4 bg-white border border-gray-200 rounded-lg shadow hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700">
<div className="flex flex-col gap-4 justify-center items-center text-2xl font-semibold text-gray-900 dark:text-white">
<img className="w-30 h-30 rounded-full" src="./brain.png" alt="brain" />
<div>
{noOfWebPages}
</div>
</div>
</div>
<button type="button" className="w-full text-white bg-gradient-to-r from-red-400 via-red-500 to-red-600 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm px-5 py-2.5 text-center" onClick={() => clearMem()}>Clear History Store</button>
<button type="button" className="w-full text-gray-900 bg-gradient-to-r from-red-200 via-red-300 to-yellow-200 hover:bg-gradient-to-bl focus:ring-4 focus:outline-none focus:ring-red-100 dark:focus:ring-red-400 font-medium rounded-lg text-sm px-5 py-2.5 text-center" onClick={() => saveCurrSnapShot()}>Save Current Webpage SnapShot</button>
<button type="button" className="w-full text-gray-900 bg-gradient-to-r from-teal-200 to-lime-200 hover:bg-gradient-to-l hover:from-teal-200 hover:to-lime-200 focus:ring-4 focus:outline-none focus:ring-lime-200 dark:focus:ring-teal-700 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2" onClick={() => saveData()}>Save to SurfSense</button>
</div>
</div>
</div>
</div>
</section>
)
}
};
const root = createRoot(document.getElementById("root")!);
root.render(
<React.StrictMode>
<Router>
<Popup />
</Router>
<ToastContainer autoClose={2000} />
</React.StrictMode>
);
// chrome.tabs.query({ active: true, currentWindow: true }, async function (tabs) {
// const tab = tabs[0];
// if (tab.id) {
// await initWebHistory(tab.id);
// await initQueues(tab.id);
// }
// });
// export const LoginForm = () => {
// const [username, setUsername] = useState('');
// const [password, setPassword] = useState('');
// const [error, setError] = useState('');
// const [loading, setLoading] = useState(false);
// // const navigate = useNavigate();
// const validateForm = () => {
// if (!username || !password) {
// setError('Username and password are required');
// return false;
// }
// setError('');
// return true;
// };
// const handleSubmit = async (event: { preventDefault: () => void; }) => {
// event.preventDefault();
// if (!validateForm()) return;
// setLoading(true);
// const formDetails = new URLSearchParams();
// formDetails.append('username', username);
// formDetails.append('password', password);
// try {
// const response = await fetch('http://localhost:8000/token', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded',
// },
// body: formDetails,
// });
// setLoading(false);
// if (response.ok) {
// const data = await response.json();
// await chrome.storage.local.set({
// token: data.access_token,
// });
// // localStorage.setItem('token', data.access_token);
// goTo(Popup);
// } else {
// const errorData = await response.json();
// setError(errorData.detail || 'Authentication failed!');
// }
// } catch (error) {
// setLoading(false);
// setError('An error occurred. Please try again later.');
// }
// };
// const goToRegister = async () => {
// console.log("Reg")
// goTo(RegisterForm)
// }
// return (
// <>
// <section className="dark bg-gray-50 dark:bg-gray-900">
// <div onClick={() => clearMem}>CLEAR MEM</div>
// <div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
// <a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
// <img className="w-8 h-8 mr-2" src={"./icon-128.png"} alt="logo" />
// SurfSense
// </a>
// <div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
// <div className="p-6 space-y-4 md:space-y-6 sm:p-8">
// <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
// Sign in to your account
// </h1>
// <form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
// <div>
// <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
// <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name" />
// </div>
// <div>
// <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
// <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} name="password" id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
// </div>
// <button type="submit" disabled={loading} className="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">{loading ? 'Logging in...' : 'Login'}</button>
// <p className="text-sm font-light text-gray-500 dark:text-gray-400">
// Dont have an account yet? <a href="#" className="font-medium text-primary-600 hover:underline dark:text-primary-500" onClick={() => goToRegister()}>Sign up</a>
// </p>
// {error && <p style={{ color: 'red' }}>{error}</p>}
// </form>
// </div>
// </div>
// </div>
// </section>
// </>
// );
// }
// import { createGlobalState } from 'react-hooks-global-state';
// const initialState = { count: 0 };
// const { useGlobalState } = createGlobalState(initialState);
// const [count, setCount] = useGlobalState('count');
// setCount(v => v - 1);
// let saveJobsObj = await chrome.storage.local.get(["savejobs"]);
// await chrome.storage.local.set({
// savejobs: saveJobsObj.savejobs - 1,
// });
// else{
// toPushInTabHistory.reffererUrl = urlQueueLocal.urlQueue[tabId][urlQueueLocal.urlQueue[tabId].length - 1]
// }
// if(!tabhistory[tabhistory.length - 1].duration){
// tabhistory[tabhistory.length - 1].duration = Date.now() - timeQueueLocal.timeQueue[tabId][timeQueueLocal.timeQueue[tabId].length - 1]
// }
// if (tabhistory.length === 0) {
// toPushInTabHistory.duration = Date.now() - timeQueueLocal.timeQueue[tabId][timeQueueLocal.timeQueue[tabId].length - 1]
// }
// else {
// }
// const lastEntryTimeObj = await chrome.storage.local.get([
// "lastEntryTime",
// ]);
// const autotrackerFlag = await chrome.storage.local.get(["autoTracker"]);
// // if (autotrackerFlag.autoTracker) {
// // let urlQueue = await chrome.storage.local.get(["urlQueue"]);
// // delete urlQueue.urlQueue[tabId];
// // }
// //When first entry
// if (tabhistory.length === 0) {
// let urlQueue = await chrome.storage.local.get(["urlQueue"]);
// if (autotrackerFlag.autoTracker) {
// toPushInTabHistory.reffererUrl = "START";
// try {
// delete urlQueue.urlQueue[tabId];
// } catch (error) {
// console.log(error);
// }
// } else {
// if (urlQueue.urlQueue[tabId].length >= 2) {
// toPushInTabHistory.reffererUrl = urlQueue.urlQueue[tabId][urlQueue.urlQueue[tabId].length - 2];
// } else {
// toPushInTabHistory.reffererUrl = "START";
// }
// }
// toPushInTabHistory.duration = Date.now() - lastEntryTimeObj.lastEntryTime[tabId];
// tabhistory.push(toPushInTabHistory);
// try {
// webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tab.id
// ).tabHistory = tabhistory;
// await chrome.storage.local.set({
// webhistory: webhistoryObj.webhistory,
// });
// } catch (error) {
// console.log(error);
// }
// } else {
// if (autotrackerFlag.autoTracker) {
// toPushInTabHistory.reffererUrl = tabhistory[tabhistory.length - 1].url;
// } else {
// let urlQueue = await chrome.storage.local.get(["urlQueue"]);
// toPushInTabHistory.reffererUrl = urlQueue.urlQueue[tabId][urlQueue.urlQueue[tabId].length - 2];
// }
// if (!tabhistory[tabhistory.length - 1].duration) {
// toPushInTabHistory.duration = Date.now() - tabhistory[tabhistory.length - 1].entryTime
// }
// toPushInTabHistory.duration = Date.now() - lastEntryTimeObj.lastEntryTime[tabId];
// tabhistory.push(toPushInTabHistory);
// try {
// webhistoryObj.webhistory.find(
// (data: WebHistory) => data.tabsessionId === tab.id
// ).tabHistory = tabhistory;
// await chrome.storage.local.set({
// webhistory: webhistoryObj.webhistory,
// });
// } catch (error) {
// console.log(error);
// }
// }

3
extension/src/sum.ts Normal file
View file

@ -0,0 +1,3 @@
export function sum(x: number, y: number) {
return x + y;
}

View file

@ -0,0 +1,60 @@
module.exports = {
content: ["./src/*.{js,jsx,ts,tsx}"],
darkMode: "selector",
theme: {
extend: {
colors: {
primary: {
50: "#eff6ff",
100: "#dbeafe",
200: "#bfdbfe",
300: "#93c5fd",
400: "#60a5fa",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
800: "#1e40af",
900: "#1e3a8a",
950: "#172554",
},
},
},
fontFamily: {
body: [
"Inter",
"ui-sans-serif",
"system-ui",
"-apple-system",
"system-ui",
"Segoe UI",
"Roboto",
"Helvetica Neue",
"Arial",
"Noto Sans",
"sans-serif",
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji",
],
sans: [
"Inter",
"ui-sans-serif",
"system-ui",
"-apple-system",
"system-ui",
"Segoe UI",
"Roboto",
"Helvetica Neue",
"Arial",
"Noto Sans",
"sans-serif",
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji",
],
},
},
plugins: [],
};

15
extension/tsconfig.json Normal file
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"strict": true,
"target": "es6",
"moduleResolution": "bundler",
"module": "ES6",
"esModuleInterop": true,
"sourceMap": false,
"rootDir": "src",
"outDir": "dist/js",
"noEmitOnError": true,
"jsx": "react",
"typeRoots": [ "node_modules/@types" ]
}
}

View file

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"moduleResolution": "node"
}
}

View file

@ -0,0 +1,66 @@
const webpack = require("webpack");
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const srcDir = path.join(__dirname, "..", "src");
const tailwindcss = require('tailwindcss')
const autoprefixer = require('autoprefixer')
module.exports = {
entry: {
popup: path.join(srcDir, 'popup.tsx'),
background: path.join(srcDir, 'background.ts'),
content_script: path.join(srcDir, 'content_script.tsx'),
},
output: {
path: path.join(__dirname, "../dist/js"),
filename: "[name].js",
},
optimization: {
splitChunks: {
name: "vendor",
chunks(chunk) {
return chunk.name !== 'background';
}
},
},
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.css$/i,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
{
loader: 'postcss-loader', // postcss loader needed for tailwindcss
options: {
postcssOptions: {
ident: 'postcss',
plugins: [tailwindcss, autoprefixer],
},
},
},
],
},
],
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
},
plugins: [
new CopyPlugin({
patterns: [{ from: ".", to: "../", context: "public" }],
options: {},
}),
],
};

View file

@ -0,0 +1,7 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'inline-source-map',
mode: 'development'
});

View file

@ -0,0 +1,6 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production'
});

36
web/.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
web/README.md Normal file
View file

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View file

@ -0,0 +1,8 @@
import React from 'react';
import MarkdownPreview from '@uiw/react-markdown-preview';
export default function MarkDownTest({source} : {source: string}) {
return (
<MarkdownPreview source={source} style={{ padding: 16 }} />
)
}

344
web/app/chat/page.tsx Normal file
View file

@ -0,0 +1,344 @@
"use client";
import React, { useEffect, useState } from "react";
import Image from "next/image";
import logo from "@/public/SurfSense.png";
import { FileCheck } from "lucide-react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Table,
TableBody,
TableCell,
TableRow,
} from "@/components/ui/table";
import MarkDownTest from "./markdown";
import { useRouter } from "next/navigation";
type Document = {
BrowsingSessionId: string;
VisitedWebPageURL: string;
VisitedWebPageTitle: string;
VisitedWebPageDateWithTimeInISOString: string;
VisitedWebPageReffererURL: string;
VisitedWebPageVisitDurationInMilliseconds: number;
VisitedWebPageContent: string;
};
// type Description = {
// response: string;
// };
// type NormalResponse = {
// response: string;
// relateddocs: Document[];
// };
// type ChatMessage = {
// type: string;
// userquery: string;
// message: NormalResponse | string;
// };
function ProtectedPage() {
// const navigate = useNavigate();
const router = useRouter();
const [loading, setLoading] = useState<boolean>(false);
const [currentChat, setCurrentChat] = useState<any[]>([]);
useEffect(() => {
const verifyToken = async () => {
const token = window.localStorage.getItem('token');
// console.log(token)
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL!}/verify-token/${token}`);
if (!response.ok) {
throw new Error('Token verification failed');
}else{
const NEO4JURL = localStorage.getItem('neourl');
const NEO4JUSERNAME = localStorage.getItem('neouser');
const NEO4JPASSWORD = localStorage.getItem('neopass');
const OPENAIKEY = localStorage.getItem('openaikey');
const check = (NEO4JURL && NEO4JUSERNAME && NEO4JPASSWORD && OPENAIKEY)
if(!check){
router.push('/settings');
}
}
} catch (error) {
window.localStorage.removeItem('token');
router.push('/login');
}
};
verifyToken();
}, [router]);
const handleSubmit = async (formData: any) => {
setLoading(true);
const query = formData.get("query");
if (!query) {
console.log("Query cant be empty!!");
return;
}
const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: query,
neourl: localStorage.getItem('neourl'),
neouser: localStorage.getItem('neouser'),
neopass: localStorage.getItem('neopass'),
openaikey: localStorage.getItem('openaikey'),
apisecretkey: process.env.NEXT_PUBLIC_API_SECRET_KEY
}),
};
fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL!}`, requestOptions)
.then(res=>res.json())
.then(data=> {
let cur = currentChat;
cur.push({
type: "normal",
userquery: query,
message: data,
});
setCurrentChat([...cur]);
setLoading(false);
});
};
const getDocDescription = async (document: Document) => {
setLoading(true);
const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: JSON.stringify(document),
neourl: localStorage.getItem('neourl'),
neouser: localStorage.getItem('neouser'),
neopass: localStorage.getItem('neopass'),
openaikey: localStorage.getItem('openaikey'),
apisecretkey: process.env.NEXT_PUBLIC_API_SECRET_KEY
}),
};
const response = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL!}/kb/doc`,
requestOptions
);
const res = await response.json();
let cur = currentChat;
cur.push({
type: "description",
doctitle: document.VisitedWebPageTitle,
message: res.response,
});
setLoading(false);
setCurrentChat([...cur]);
// console.log(document);
};
if (currentChat) {
return (
<>
<div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden mt-16">
<div className="group w-full overflow-auto pl-0 peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]">
<div className="pb-[200px] pt-4 md:pt-10">
<div className="mx-auto max-w-4xl px-4 flex flex-col gap-3">
<div className="bg-background flex flex-col gap-2 rounded-lg border p-8">
<h1 className="text-sm font-semibold">
Welcome to SurfSense
</h1>
<p className="text-muted-foreground leading-normal">
🧠 Ask Your Knowledge Graph Brain 🧠
</p>
</div>
{currentChat.map((chat, index) => {
// console.log("chat", chat);
if (chat.type === "normal") {
return (
<div
className="bg-background flex flex-col gap-2 rounded-lg border p-8"
key={index}
>
<p className="text-3xl font-semibold">
{chat.userquery}
</p>
<p className="font-sm font-semibold">
SurfSense Response:
</p>
<MarkDownTest source={chat.message.response} />
<p className="font-sm font-semibold">
Related Browsing Sessions
</p>
{
//@ts-ignore
chat.message.relateddocs.map((doc) => {
return (
<Collapsible className="border rounded-lg p-3">
<CollapsibleTrigger className="flex justify-between gap-2 mb-2">
<FileCheck />
{doc.VisitedWebPageTitle}
</CollapsibleTrigger>
<CollapsibleContent className="flex flex-col gap-4">
<Table>
<TableBody>
<TableRow>
<TableCell className="font-medium">
Browsing Session Id
</TableCell>
<TableCell>
{doc.BrowsingSessionId}
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">
URL
</TableCell>
<TableCell>
{doc.VisitedWebPageURL}
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">
Reffering URL
</TableCell>
<TableCell>
{doc.VisitedWebPageReffererURL}
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">
Date & Time Visited
</TableCell>
<TableCell>
{
doc.VisitedWebPageDateWithTimeInISOString
}
</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">
Visit Duration (In Milliseconds)
</TableCell>
<TableCell>
{
doc.VisitedWebPageVisitDurationInMilliseconds
}
</TableCell>
</TableRow>
</TableBody>
</Table>
<button
type="button"
onClick={() => getDocDescription(doc)}
className="text-gray-900 w-full hover:text-white border border-gray-800 hover:bg-gray-900 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 mb-2 dark:border-gray-600 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-800"
>
Get More Information
</button>
</CollapsibleContent>
</Collapsible>
);
})
}
</div>
);
}
if (chat.type === "description") {
return (
<div
className="bg-background flex flex-col gap-2 rounded-lg border p-8"
key={index}
>
<p className="text-3xl font-semibold">
{chat.doctitle}
</p>
<MarkDownTest source={chat.message} />
</div>
);
}
})}
</div>
<div className="h-px w-full"></div>
</div>
<div className="from-muted/30 to-muted/30 animate-in dark:from-background/10 dark:to-background/80 inset-x-0 bottom-0 w-full duration-300 ease-in-out peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px] dark:from-10%">
<div className="mx-auto sm:max-w-4xl sm:px-4">
<div className={loading ? "rounded-md p-4 w-full my-4" : "hidden"}>
<div className="animate-pulse flex space-x-4">
<div className="rounded-full bg-slate-700 h-10 w-10">
</div>
<div className="flex-1 space-y-6 py-1">
<div className="h-2 bg-slate-700 rounded"></div>
<div className="space-y-3">
<div className="grid grid-cols-3 gap-4">
<div className="h-2 bg-slate-700 rounded col-span-2"></div>
<div className="h-2 bg-slate-700 rounded col-span-1"></div>
</div>
<div className="h-2 bg-slate-700 rounded"></div>
</div>
</div>
</div>
</div>
<div className="bg-background space-y-4 border-t px-4 py-2 shadow-lg sm:rounded-t-xl sm:border md:py-4">
<form action={handleSubmit}>
<div className="bg-background relative flex max-h-60 w-full grow flex-col overflow-hidden px-8 sm:rounded-md sm:border sm:px-12">
<Image
className="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input shadow-sm hover:bg-accent hover:text-accent-foreground h-9 w-9 bg-background absolute left-0 top-[13px] size-8 rounded-full p-0 sm:left-4"
src={logo}
alt="aiicon"
/>
<span className="sr-only">New Chat</span>
<textarea
placeholder="Send a message."
className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
name="query"
></textarea>
<div className="absolute right-0 top-[13px] sm:right-4">
<button
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 w-9"
type="submit"
data-state="closed"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
fill="currentColor"
className="size-4"
>
<path d="M200 32v144a8 8 0 0 1-8 8H67.31l34.35 34.34a8 8 0 0 1-11.32 11.32l-48-48a8 8 0 0 1 0-11.32l48-48a8 8 0 0 1 11.32 11.32L67.31 168H184V32a8 8 0 0 1 16 0Z"></path>
</svg>
<span className="sr-only">Send message</span>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</>
);
}
}
export default ProtectedPage;

BIN
web/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

69
web/app/globals.css Normal file
View file

@ -0,0 +1,69 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

73
web/app/layout.tsx Normal file
View file

@ -0,0 +1,73 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { MainNavbar } from "@/components/homepage/NavBar";
import { Toaster } from "@/components/ui/toaster"
import { Footer } from "@/components/homepage/Footer";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "SurfSense - A Knowledge Graph Brain for World Wide Web Surfers.",
description:
"Save anything you see or browse on the Internet and save it to ask AI about it.",
openGraph: {
images: [
{
url: "https://surfsense.net/og-image.png",
width: 1200,
height: 627,
alt: "SurfSense - A Knowledge Graph Brain for World Wide Web Surfers.",
},
],
},
twitter: {
card: "summary_large_image",
site: "https://surfsense.net",
creator: "https://surfsense.net",
title: "SurfSense - A Knowledge Graph Brain for World Wide Web Surfers.",
description:
"Save anything you see or browse on the Internet and save it to ask AI about it.",
images: [
{
url: "https://surfsense.net/og-image.png",
width: 1200,
height: 627,
alt: "SurfSense - A Knowledge Graph Brain for World Wide Web Surfers.",
},
],
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<div className="flex flex-col">
<MainNavbar />
<div className="grow">
{children}
</div>
<Footer />
</div>
<Toaster />
</ThemeProvider>
</body>
</html>
);
}

9
web/app/login/page.tsx Normal file
View file

@ -0,0 +1,9 @@
import { LoginForm } from "@/components/logins/LoginForm"
const page = () => {
return (
<><LoginForm /></>
)
}
export default page

10
web/app/page.tsx Normal file
View file

@ -0,0 +1,10 @@
import { HomePage } from "@/components/homepage/HomePage";
export default function Home() {
return (
<>
<HomePage />
</>
);
}

80
web/app/settings/page.tsx Normal file
View file

@ -0,0 +1,80 @@
"use client"
import React, { useState } from "react";
import { useRouter } from "next/navigation";
const FillEnvVariables = () => {
const [neourl, setNeourl] = useState('');
const [neouser, setNeouser] = useState('');
const [neopass, setNeopass] = useState('');
const [openaikey, setOpenaiKey] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
const validateForm = () => {
if (!neourl || !neouser || !neopass || !openaikey) {
setError('All values are required');
return false;
}
setError('');
return true;
};
const handleSubmit = async (event: { preventDefault: () => void; }) => {
event.preventDefault();
if (!validateForm()) return;
setLoading(true);
localStorage.setItem('neourl', neourl);
localStorage.setItem('neouser', neouser);
localStorage.setItem('neopass', neopass);
localStorage.setItem('openaikey', openaikey);
setLoading(false);
router.push('/chat')
};
return (
<section className="bg-gray-50 dark:bg-gray-900">
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
SurfSense
</a>
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Required Values
</h1>
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J URL</label>
<input type="text" value={neourl} onChange={(e) => setNeourl(e.target.value)} name="neourl" id="neourl" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Username</label>
<input type="text" value={neouser} onChange={(e) => setNeouser(e.target.value)} name="neouser" id="neouser" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Password</label>
<input type="text" value={neopass} onChange={(e) => setNeopass(e.target.value)} name="neopass" id="neopass" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">OpenAI API Key</label>
<input type="text" value={openaikey} onChange={(e) => setOpenaiKey(e.target.value)} name="openaikey" id="openaikey" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
</div>
<button type="submit" className="mt-4 w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">{loading ? 'Saving....' : 'Save & Proceed'}</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
</div>
</div>
</div>
</section>
)
}
export default FillEnvVariables

9
web/app/signup/page.tsx Normal file
View file

@ -0,0 +1,9 @@
import { RegisterForm } from "@/components/logins/RegisterForm"
const page = () => {
return (
<RegisterForm />
)
}
export default page

20
web/components.json Normal file
View file

@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"examples": "@/components/examples",
"blocks": "@/components/blocks"
}
}

View file

@ -0,0 +1,43 @@
import React from "react";
import { useToast } from "../ui/use-toast";
export const FillEnvVariables = () => {
const { toast } = useToast()
return (
<section>
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
SurfSense
</a>
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Required Values
</h1>
<form className="space-y-4 md:space-y-6" action="#">
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J URL</label>
<input type="email" name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Username</label>
<input type="email" name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Neo4J Password</label>
<input type="password" name="password" id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">OpenAI API Key</label>
<input type="email" name="email" id="email" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
</div>
<button type="submit" className="mt-4 w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">Save & Proceed</button>
</form>
</div>
</div>
</div>
</section>
)
}

View file

@ -0,0 +1,10 @@
export const Footer = () => {
return (
<footer className="mt-2 w-full md:flex overflow-y-hidden items-center justify-between gap-4 px-8 py-8 text-sm text-zinc-500 overflow-hidden text-center">
<p>© 2024 SurfSense.net</p>
<div className="flex gap-5 justify-around my-2">
<a className="group/mail flex items-center" target="_blank" href="mailto:hi@dhravya.dev">Contact<svg className="group-hover/mail:opacity-100 opacity-0 transition hidden md:block" width="24px" height="24px" viewBox="-2.4 -2.4 28.80 28.80" fill="none" xmlns="http://www.w3.org/2000/svg" transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M7 17L17 7M17 7H8M17 7V16" stroke="currentColor" stroke-width="0.792" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg></a><a className="group/twit flex items-center" target="_blank" href="https://twitter.com/supermemoryai">Twitter<svg className="group-hover/twit:opacity-100 opacity-0 transition hidden md:block" width="24px" height="24px" viewBox="-2.4 -2.4 28.80 28.80" fill="none" xmlns="http://www.w3.org/2000/svg" transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M7 17L17 7M17 7H8M17 7V16" stroke="currentColor" stroke-width="0.792" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg></a><a className="group/git flex items-center" target="_blank" href="https://github.com/dhravya/supermemory">Github<svg className="group-hover/git:opacity-100 opacity-0 transition hidden md:block" width="24px" height="24px" viewBox="-2.4 -2.4 28.80 28.80" fill="none" xmlns="http://www.w3.org/2000/svg" transform="matrix(1, 0, 0, 1, 0, 0)rotate(0)"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M7 17L17 7M17 7H8M17 7V16" stroke="currentColor" stroke-width="0.792" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg></a>
</div>
</footer>
)
}

View file

@ -0,0 +1,43 @@
"use client";
import { motion } from "framer-motion";
import React from "react";
import { AuroraBackground } from "../ui/aurora-background";
import icon from "../../public/SurfSense.png"
import Image from "next/image";
import Link from "next/link";
export function HomePage() {
return (
<AuroraBackground>
<motion.div
initial={{ opacity: 0.0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{
delay: 0.3,
duration: 0.8,
ease: "easeInOut",
}}
className="relative flex flex-col gap-4 items-center justify-center px-4"
>
<div className="flex items-center mb-4 text-5xl font-semibold text-gray-900 dark:text-white">
<Image className="w-64 h-64 rounded-full" src={icon} alt="logo" />
</div>
<div className="text-3xl md:text-7xl font-bold dark:text-white text-center">
SurfSense
</div>
{/* <div className="text-lg font-semibold dark:text-neutral-200">Beta v0.0.1</div> */}
<div className="font-extralight text-base md:text-4xl dark:text-neutral-200 py-4">
A Knowledge Graph 🧠 Brain 🧠 for World Wide Web Surfers.
</div>
<button className="relative inline-flex h-12 overflow-hidden rounded-full p-[1px] focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50">
<span className="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#E2CBFF_0%,#393BB2_50%,#E2CBFF_100%)]" />
<Link href={'/signup'} className="inline-flex h-full w-full cursor-pointer items-center justify-center rounded-full bg-slate-950 px-8 py-4 text-2xl font-medium text-white backdrop-blur-3xl">
Sign Up
</Link>
</button>
</motion.div>
</AuroraBackground>
);
}

View file

@ -0,0 +1,63 @@
"use client";
import React, { useState } from "react";
import { HoveredLink, Menu, MenuItem, ProductItem } from "../ui/navbar-menu";
import { cn } from "@/lib/utils";
import { ThemeToggle } from "./theme-toggle";
import Image from "next/image";
import logo from "../../public/SurfSense.png"
import Link from "next/link";
export function MainNavbar() {
return (
<div className="relative w-full flex items-center justify-around">
<Navbar className="top-2 px-2" />
</div>
);
}
function Navbar({ className }: { className?: string }) {
const [active, setActive] = useState<string | null>(null);
return (
<div
className={cn("fixed top-10 inset-x-0 max-w-7xl mx-auto z-50", className)}
>
<Menu setActive={setActive}>
<Link href={"/"} className="flex items-center text-2xl font-semibold text-gray-900 dark:text-white">
<Image className="hidden sm:block w-8 h-8 mr-2" src={logo} alt="logo" />
<span className="hidden md:block">SurfSense</span>
</Link>
<div className="flex gap-2">
<Link href={"/login"}>
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
Log In
</button>
</Link>
<Link href={"/signup"}>
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
Sign Up
</button>
</Link>
<Link href={"/settings"}>
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
Settings
</button>
</Link>
<Link href={"/chat"} className="grow">
<button className="px-4 py-2 rounded-md border border-black bg-white text-black text-sm hover:shadow-[4px_4px_0px_0px_rgba(0,0,0)] transition duration-200">
🧠
</button>
</Link>
<ThemeToggle />
</div>
</Menu>
</div>
);
}

View file

@ -0,0 +1,40 @@
"use client"
import * as React from "react"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function ThemeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="bg-transparent" variant="ghost" size="sm">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View file

@ -0,0 +1,107 @@
"use client"
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
export const LoginForm = () => {
const router = useRouter();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const validateForm = () => {
if (!username || !password) {
setError('Username and password are required');
return false;
}
setError('');
return true;
};
const handleSubmit = async (event: any) => {
event.preventDefault();
if (!validateForm()) return;
setLoading(true);
const formDetails = new URLSearchParams();
formDetails.append('username', username);
formDetails.append('password', password);
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL!}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formDetails,
});
setLoading(false);
if (response.ok) {
const data = await response.json();
window.localStorage.setItem('token', data.access_token);
router.push('/chat');
// navigate('/protected');
} else {
const errorData = await response.json();
setError(errorData.detail || 'Authentication failed!');
}
} catch (error) {
setLoading(false);
setError('An error occurred. Please try again later.');
}
};
return (
<>
<section>
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<a href="#" className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img className="w-8 h-8 mr-2" src={"./icon-128.png"} alt="logo" />
SurfSense
</a>
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Sign in to your account
</h1>
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Username</label>
<input name="email" id="email" value={username}
onChange={(e) => setUsername(e.target.value)} className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
<input
type="password"
name="password"
id="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
/>
</div>
<button type="submit" className="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
{loading ? 'Logging in...' : 'Login'}
</button>
<p className="text-sm font-light text-gray-500 dark:text-gray-400">
Dont have an account yet? <Link href={"/signup"} className="font-medium text-primary-600 hover:underline dark:text-primary-500">Sign up</Link>
</p>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
</div>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,112 @@
"use client"
import React, { FormEvent, useState } from "react";
import ReCAPTCHA from "react-google-recaptcha";
import { useRouter } from "next/navigation";
import { useToast } from "../ui/use-toast";
import Link from "next/link";
export const RegisterForm = () => {
const [captcha, setCaptcha] = useState<string | null>();
const router = useRouter();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [confpassword, setConfPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { toast } = useToast()
const validateForm = () => {
if (!username || !password || !confpassword) {
setError('Username and password are required');
return false;
}
setError('');
return true;
};
const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
setLoading(true);
if (captcha) {
if (!validateForm()) return;
try {
const toSend = {
username: username,
password: password,
apisecretkey: process.env.NEXT_PUBLIC_API_SECRET_KEY!
}
const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL!}/register`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(toSend),
});
setLoading(false);
if (response.ok) {
toast({
title: "Registered Successfully",
description: "Redirecting to Login",
})
router.push('/login');
} else {
const errorData = await response.json();
setError(errorData.detail || 'Authentication failed!');
}
} catch (error) {
setLoading(false);
setError('An error occurred. Please try again later.');
}
} else {
setError('Recaptcha Failed');
}
}
return (
<section>
<div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div className="flex items-center mb-6 text-2xl font-semibold text-gray-900 dark:text-white">
<img className="w-8 h-8 mr-2" src="./icon-128.png" alt="logo" />
SurfSense
</div>
<div className="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div className="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Create an account
</h1>
<form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
<input value={username}
onChange={(e) => setUsername(e.target.value)} type="username" name="username" id="username" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="name@company.com" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
<input value={password}
onChange={(e) => setPassword(e.target.value)} type="password" name="password" id="password" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
</div>
<div>
<label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
<input value={confpassword}
onChange={(e) => setConfPassword(e.target.value)}
type="confirm-password" name="confpassword" id="confpassword" placeholder="••••••••" className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" />
</div>
<ReCAPTCHA sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY!} className="mx-auto" onChange={setCaptcha} />
<button type="submit" className="mt-4 w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"> {loading ? 'Creating...' : 'Create Account'}</button>
<p className="text-sm font-light text-gray-500 dark:text-gray-400">
Already have an account? <Link href={"/login"} className="font-medium text-primary-600 hover:underline dark:text-primary-500">Login here</Link>
</p>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
</div>
</div>
</div>
</section>
)
}

View file

@ -0,0 +1,9 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View file

@ -0,0 +1,54 @@
"use client";
import { cn } from "@/lib/utils";
import React, { ReactNode } from "react";
interface AuroraBackgroundProps extends React.HTMLProps<HTMLDivElement> {
children: ReactNode;
showRadialGradient?: boolean;
}
export const AuroraBackground = ({
className,
children,
showRadialGradient = true,
...props
}: AuroraBackgroundProps) => {
return (
<main>
<div
className={cn(
"relative flex flex-col h-[100vh] items-center justify-center bg-zinc-50 dark:bg-zinc-900 text-slate-950 transition-bg",
className
)}
{...props}
>
<div className="absolute inset-0 overflow-hidden">
<div
// I'm sorry but this is what peak developer performance looks like // trigger warning
className={cn(
`
[--white-gradient:repeating-linear-gradient(100deg,var(--white)_0%,var(--white)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--white)_16%)]
[--dark-gradient:repeating-linear-gradient(100deg,var(--black)_0%,var(--black)_7%,var(--transparent)_10%,var(--transparent)_12%,var(--black)_16%)]
[--aurora:repeating-linear-gradient(100deg,var(--blue-500)_10%,var(--indigo-300)_15%,var(--blue-300)_20%,var(--violet-200)_25%,var(--blue-400)_30%)]
[background-image:var(--white-gradient),var(--aurora)]
dark:[background-image:var(--dark-gradient),var(--aurora)]
[background-size:300%,_200%]
[background-position:50%_50%,50%_50%]
filter blur-[10px] invert dark:invert-0
after:content-[""] after:absolute after:inset-0 after:[background-image:var(--white-gradient),var(--aurora)]
after:dark:[background-image:var(--dark-gradient),var(--aurora)]
after:[background-size:200%,_100%]
after:animate-aurora after:[background-attachment:fixed] after:mix-blend-difference
pointer-events-none
absolute -inset-[10px] opacity-50 will-change-transform`,
showRadialGradient &&
`[mask-image:radial-gradient(ellipse_at_100%_0%,black_10%,var(--transparent)_70%)]`
)}
></div>
</div>
{children}
</div>
</main>
);
};

View file

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View file

@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View file

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

104
web/components/ui/lamp.tsx Normal file
View file

@ -0,0 +1,104 @@
"use client";
import React from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
export default function LampDemo() {
return (
<LampContainer>
<motion.h1
initial={{ opacity: 0.5, y: 100 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{
delay: 0.3,
duration: 0.8,
ease: "easeInOut",
}}
className="mt-8 bg-gradient-to-br from-slate-300 to-slate-500 py-4 bg-clip-text text-center text-4xl font-medium tracking-tight text-transparent md:text-7xl"
>
Build lamps <br /> the right way
</motion.h1>
</LampContainer>
);
}
export const LampContainer = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<div
className={cn(
"relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-slate-950 w-full rounded-md z-0",
className
)}
>
<div className="relative flex w-full flex-1 scale-y-125 items-center justify-center isolate z-0 ">
<motion.div
initial={{ opacity: 0.5, width: "15rem" }}
whileInView={{ opacity: 1, width: "30rem" }}
transition={{
delay: 0.3,
duration: 0.8,
ease: "easeInOut",
}}
style={{
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
}}
className="absolute inset-auto right-1/2 h-56 overflow-visible w-[30rem] bg-gradient-conic from-cyan-500 via-transparent to-transparent text-white [--conic-position:from_70deg_at_center_top]"
>
<div className="absolute w-[100%] left-0 bg-slate-950 h-40 bottom-0 z-20 [mask-image:linear-gradient(to_top,white,transparent)]" />
<div className="absolute w-40 h-[100%] left-0 bg-slate-950 bottom-0 z-20 [mask-image:linear-gradient(to_right,white,transparent)]" />
</motion.div>
<motion.div
initial={{ opacity: 0.5, width: "15rem" }}
whileInView={{ opacity: 1, width: "30rem" }}
transition={{
delay: 0.3,
duration: 0.8,
ease: "easeInOut",
}}
style={{
backgroundImage: `conic-gradient(var(--conic-position), var(--tw-gradient-stops))`,
}}
className="absolute inset-auto left-1/2 h-56 w-[30rem] bg-gradient-conic from-transparent via-transparent to-cyan-500 text-white [--conic-position:from_290deg_at_center_top]"
>
<div className="absolute w-40 h-[100%] right-0 bg-slate-950 bottom-0 z-20 [mask-image:linear-gradient(to_left,white,transparent)]" />
<div className="absolute w-[100%] right-0 bg-slate-950 h-40 bottom-0 z-20 [mask-image:linear-gradient(to_top,white,transparent)]" />
</motion.div>
<div className="absolute top-1/2 h-48 w-full translate-y-12 scale-x-150 bg-slate-950 blur-2xl"></div>
<div className="absolute top-1/2 z-50 h-48 w-full bg-transparent opacity-10 backdrop-blur-md"></div>
<div className="absolute inset-auto z-50 h-36 w-[28rem] -translate-y-1/2 rounded-full bg-cyan-500 opacity-50 blur-3xl"></div>
<motion.div
initial={{ width: "8rem" }}
whileInView={{ width: "16rem" }}
transition={{
delay: 0.3,
duration: 0.8,
ease: "easeInOut",
}}
className="absolute inset-auto z-30 h-36 w-64 -translate-y-[6rem] rounded-full bg-cyan-400 blur-2xl"
></motion.div>
<motion.div
initial={{ width: "15rem" }}
whileInView={{ width: "30rem" }}
transition={{
delay: 0.3,
duration: 0.8,
ease: "easeInOut",
}}
className="absolute inset-auto z-50 h-0.5 w-[30rem] -translate-y-[7rem] bg-cyan-400 "
></motion.div>
<div className="absolute inset-auto z-40 h-44 w-full -translate-y-[12.5rem] bg-slate-950 "></div>
</div>
<div className="relative z-50 flex -translate-y-80 flex-col items-center px-5">
{children}
</div>
</div>
);
};

View file

@ -0,0 +1,121 @@
"use client";
import React from "react";
import { motion } from "framer-motion";
import Link from "next/link";
import Image from "next/image";
const transition = {
type: "spring",
mass: 0.5,
damping: 11.5,
stiffness: 100,
restDelta: 0.001,
restSpeed: 0.001,
};
export const MenuItem = ({
setActive,
active,
item,
children,
}: {
setActive: (item: string) => void;
active: string | null;
item: string;
children?: React.ReactNode;
}) => {
return (
<div onMouseEnter={() => setActive(item)} className="relative ">
<motion.p
transition={{ duration: 0.3 }}
className="cursor-pointer text-black hover:opacity-[0.9] dark:text-white"
>
{item}
</motion.p>
{active !== null && (
<motion.div
initial={{ opacity: 0, scale: 0.85, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={transition}
>
{active === item && (
<div className="absolute top-[calc(100%_+_1.2rem)] left-1/2 transform -translate-x-1/2 pt-4">
<motion.div
transition={transition}
layoutId="active" // layoutId ensures smooth animation
className="bg-white dark:bg-black backdrop-blur-sm rounded-2xl overflow-hidden border border-black/[0.2] dark:border-white/[0.2] shadow-xl"
>
<motion.div
layout // layout ensures smooth animation
className="w-max h-full p-4"
>
{children}
</motion.div>
</motion.div>
</div>
)}
</motion.div>
)}
</div>
);
};
export const Menu = ({
setActive,
children,
}: {
setActive: (item: string | null) => void;
children: React.ReactNode;
}) => {
return (
<nav
onMouseLeave={() => setActive(null)} // resets the state
className="relative rounded-full border dark:bg-black/20 dark:border-white/[0.2] bg-white/20 shadow-input flex justify-center md:justify-between space-x-4 px-10 py-4 place-items-center backdrop-blur-lg"
>
{children}
</nav>
);
};
export const ProductItem = ({
title,
description,
href,
src,
}: {
title: string;
description: string;
href: string;
src: string;
}) => {
return (
<Link href={href} className="flex space-x-2">
<Image
src={src}
width={140}
height={70}
alt={title}
className="flex-shrink-0 rounded-md shadow-2xl"
/>
<div>
<h4 className="text-xl font-bold mb-1 text-black dark:text-white">
{title}
</h4>
<p className="text-neutral-700 text-sm max-w-[10rem] dark:text-neutral-300">
{description}
</p>
</div>
</Link>
);
};
export const HoveredLink = ({ children, ...rest }: any) => {
return (
<Link
{...rest}
className="text-neutral-700 dark:text-neutral-200 hover:text-black "
>
{children}
</Link>
);
};

117
web/components/ui/table.tsx Normal file
View file

@ -0,0 +1,117 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

129
web/components/ui/toast.tsx Normal file
View file

@ -0,0 +1,129 @@
"use client"
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View file

@ -0,0 +1,35 @@
"use client"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View file

@ -0,0 +1,194 @@
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

6
web/lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

4
web/next.config.mjs Normal file
View file

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

41
web/package.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@types/react-google-recaptcha": "^2.1.9",
"@uiw/react-markdown-preview": "^5.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"framer-motion": "^11.3.24",
"lucide-react": "^0.426.0",
"next": "14.2.5",
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18",
"react-element-to-jsx-string": "^15.0.0",
"react-google-recaptcha": "^3.1.0",
"react-markdown": "^9.0.1",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.9",
"typescript": "^5"
}
}

3428
web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

8
web/postcss.config.mjs Normal file
View file

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

BIN
web/public/SurfSense.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
web/public/icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

160
web/tailwind.config.ts Normal file
View file

@ -0,0 +1,160 @@
import type { Config } from "tailwindcss";
const {
default: flattenColorPalette,
} = require("tailwindcss/lib/util/flattenColorPalette");
const config = {
darkMode: ["class"],
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
"50": "#eff6ff",
"100": "#dbeafe",
"200": "#bfdbfe",
"300": "#93c5fd",
"400": "#60a5fa",
"500": "#3b82f6",
"600": "#2563eb",
"700": "#1d4ed8",
"800": "#1e40af",
"900": "#1e3a8a",
"950": "#172554",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
aurora: {
from: {
backgroundPosition: "50% 50%, 50% 50%",
},
to: {
backgroundPosition: "350% 50%, 350% 50%",
},
},
shimmer: {
from: {
backgroundPosition: "0 0",
},
to: {
backgroundPosition: "-200% 0",
},
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
aurora: "aurora 60s linear infinite",
shimmer: "shimmer 2s linear infinite",
},
fontFamily: {
'body': [
'Inter',
'ui-sans-serif',
'system-ui',
'-apple-system',
'system-ui',
'Segoe UI',
'Roboto',
'Helvetica Neue',
'Arial',
'Noto Sans',
'sans-serif',
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol',
'Noto Color Emoji'
],
'sans': [
'Inter',
'ui-sans-serif',
'system-ui',
'-apple-system',
'system-ui',
'Segoe UI',
'Roboto',
'Helvetica Neue',
'Arial',
'Noto Sans',
'sans-serif',
'Apple Color Emoji',
'Segoe UI Emoji',
'Segoe UI Symbol',
'Noto Color Emoji'
]
}
}
},
plugins: [require("tailwindcss-animate"), addVariablesForColors],
} satisfies Config;
// This plugin adds each Tailwind color as a global CSS variable, e.g. var(--gray-200).
function addVariablesForColors({ addBase, theme }: any) {
let allColors = flattenColorPalette(theme("colors"));
let newVars = Object.fromEntries(
Object.entries(allColors).map(([key, val]) => [`--${key}`, val])
);
addBase({
":root": newVars,
});
}
export default config;

26
web/tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}